revise all
This commit is contained in:
commit
bf68826881
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
18
.prettierrc
Normal file
18
.prettierrc
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-svelte",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
14
components.json
Normal file
14
components.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils"
|
||||||
|
},
|
||||||
|
"typescript": true
|
||||||
|
}
|
2
justfile
Normal file
2
justfile
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
genpb:
|
||||||
|
protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/lib/protobuf/ ./src/lib/protobuf/definitions/*
|
2927
package-lock.json
generated
Normal file
2927
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "niming-v2",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify/json": "^2.2.291",
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwind-variants": "^0.3.0",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"unplugin-icons": "^0.22.0",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobuf-ts/plugin": "^2.9.4",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"bits-ui": "^0.22.0",
|
||||||
|
"dompurify": "^3.2.3",
|
||||||
|
"mode-watcher": "^0.5.0",
|
||||||
|
"ts-proto": "^2.6.0"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
31
src/app.css
Normal file
31
src/app.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
@import "./color.css";
|
||||||
|
@import "./fonts.css";
|
||||||
|
@import "./bits-ui.css";
|
||||||
|
@import "https://cdn.plyr.io/3.7.8/plyr.css";
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* @apply bg-background text-foreground; */
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
font-family: "PingFang";
|
||||||
|
width: 100dvw;
|
||||||
|
min-height: 100dvh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: hsl(var(--secondary))
|
||||||
|
}
|
||||||
|
|
||||||
|
margin:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/app.d.ts
vendored
Normal file
14
src/app.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
import 'unplugin-icons/types/svelte'
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { };
|
12
src/app.html
Normal file
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
src/bits-ui.css
Normal file
30
src/bits-ui.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
@import "./color.css";
|
||||||
|
|
||||||
|
.post_trigger {
|
||||||
|
padding: 10px 20px;
|
||||||
|
max-height: 60dvh;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
backdrop-filter: grayscale(30%) brightness(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_content {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 50;
|
||||||
|
background-color: #0f291f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll>div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
65
src/color.css
Normal file
65
src/color.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 192, 9.0%, 32.7%;
|
||||||
|
--foreground: 47, 60.0%, 89.2%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 105 7% 43%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 192, 9.0%, 32.7%;
|
||||||
|
--foreground: 47, 60.0%, 89.2%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 105 7% 43%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
35
src/fonts.css
Normal file
35
src/fonts.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "PingFang";
|
||||||
|
src:
|
||||||
|
local('PingFang Bold'), src("/PingFang Bold.woff2") type("woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PingFang";
|
||||||
|
src:
|
||||||
|
local('PingFang Medium'), src("/PingFang Medium.woff2") type("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PingFang";
|
||||||
|
src:
|
||||||
|
local('PingFang Heavy'), src("/PingFang Heavy.woff2") type("woff2");
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PingFang";
|
||||||
|
src:
|
||||||
|
local('PingFang Light'), src("/PingFang Light.woff2") type("woff2");
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PingFang";
|
||||||
|
src:
|
||||||
|
local('PingFang Regular'), src("/PingFang Regular.woff2") type("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
21
src/lib/Button.svelte
Normal file
21
src/lib/Button.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { MouseEventHandler } from 'svelte/elements';
|
||||||
|
let {
|
||||||
|
children,
|
||||||
|
onclick = undefined,
|
||||||
|
description = 'default text'
|
||||||
|
}: {
|
||||||
|
onclick: MouseEventHandler<HTMLButtonElement> | undefined;
|
||||||
|
children: Snippet;
|
||||||
|
description: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<button {onclick}> {@render children()}</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>{description}</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
136
src/lib/Editor.svelte
Normal file
136
src/lib/Editor.svelte
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import ScrollArea from './components/ui/scroll-area/scroll-area.svelte';
|
||||||
|
import Textarea from './components/ui/textarea/textarea.svelte';
|
||||||
|
import { Dialog } from 'bits-ui';
|
||||||
|
import MynauiImageRectangle from '~icons/mynaui/image-rectangle';
|
||||||
|
import MynauiAddQueueSolid from '~icons/mynaui/add-queue-solid';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
let { mode } = $props();
|
||||||
|
|
||||||
|
let post_message = $state('');
|
||||||
|
|
||||||
|
const save_draft = () => {
|
||||||
|
//determine cookie id
|
||||||
|
let kv_pair = document.cookie.split(';').map((e) => e.trimStart());
|
||||||
|
let last_id = kv_pair.findLast((e) => e.startsWith('postdraft'));
|
||||||
|
//default id = 0
|
||||||
|
let id = 0;
|
||||||
|
// find last id and plus one
|
||||||
|
if (last_id) {
|
||||||
|
id = Number.parseInt(last_id.split('=')[0].split('_')[1]) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let draft_message = window.btoa(post_message).replaceAll('=', '#');
|
||||||
|
|
||||||
|
//save
|
||||||
|
console.log(`postdraft_${id}=${draft_message}`);
|
||||||
|
document.cookie = `postdraft_${id}=${draft_message}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let locker = false;
|
||||||
|
const debounce = (callback: Function) => {
|
||||||
|
if (!locker) {
|
||||||
|
locker = true;
|
||||||
|
callback();
|
||||||
|
setTimeout(() => {
|
||||||
|
locker = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
let selected_images = new SvelteMap<string, File>();
|
||||||
|
|
||||||
|
const image_to_url = $derived.by(() => {
|
||||||
|
let name_with_url: Map<string, string> = new Map<string, string>();
|
||||||
|
for (const file of selected_images.values()) {
|
||||||
|
name_with_url.set(file.name, URL.createObjectURL(file));
|
||||||
|
}
|
||||||
|
return name_with_url;
|
||||||
|
});
|
||||||
|
|
||||||
|
const add_image = (_: MouseEvent) => {
|
||||||
|
console.log('add_image');
|
||||||
|
// pseudo input element
|
||||||
|
let file_input = document.createElement('input');
|
||||||
|
file_input.type = 'file';
|
||||||
|
file_input.accept = '.gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp';
|
||||||
|
file_input.multiple = true;
|
||||||
|
file_input.classList.add('hidden');
|
||||||
|
file_input.onchange = () => {
|
||||||
|
for (let iter = 0; iter < (file_input.files ? file_input.files.length : 0); iter++) {
|
||||||
|
const file = file_input.files?.item(iter);
|
||||||
|
if (file) {
|
||||||
|
selected_images.set(file.name, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
file_input.onclose = () => {
|
||||||
|
file_input.remove();
|
||||||
|
};
|
||||||
|
file_input.click();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if mode === 'wide'}
|
||||||
|
<div
|
||||||
|
class="border-box mb-7 h-[60dvh] self-end rounded-md bg-primary px-3 pt-1.5 text-primary-foreground"
|
||||||
|
style="margin-inline: 7.5%"
|
||||||
|
>
|
||||||
|
<div class="relative h-10">
|
||||||
|
<button class="absolute left-0" onclickcapture={() => debounce(save_draft)}>儲存</button>
|
||||||
|
<button class="absolute right-0">送出</button>
|
||||||
|
</div>
|
||||||
|
<Textarea placeholder="匿名訊息" class="h-2/3 resize-none" bind:value={post_message}></Textarea>
|
||||||
|
<div class="text-2xl">
|
||||||
|
<!-- <button aria-label="add image"><MynauiImageRectangle></MynauiImageRectangle></button> -->
|
||||||
|
<Button description="附加圖片" onclick={add_image}
|
||||||
|
><MynauiImageRectangle></MynauiImageRectangle></Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ScrollArea>
|
||||||
|
<div class="flex w-max flex-row gap-3">
|
||||||
|
{#each image_to_url as map}
|
||||||
|
<img src={map[1]} class="rounded-md md:h-10" alt={map[0]} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if mode === 'compact'}
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger class="fixed bottom-10 right-10">
|
||||||
|
<MynauiAddQueueSolid class="text-3xl" />
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="fixed inset-0 z-50 bg-black/50" />
|
||||||
|
<Dialog.Content
|
||||||
|
class="fixed left-[50%] top-[50%] z-50 h-fit w-[85dvw] -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-popover px-3 py-2"
|
||||||
|
>
|
||||||
|
<div class="relative h-10">
|
||||||
|
<button class="absolute left-0" onclickcapture={() => debounce(save_draft)}>儲存</button>
|
||||||
|
<button class="absolute right-0">送出</button>
|
||||||
|
</div>
|
||||||
|
<Textarea placeholder="匿名訊息" class="min-h-40 resize-none" bind:value={post_message}
|
||||||
|
></Textarea>
|
||||||
|
<div class="text-2xl">
|
||||||
|
<!-- <button aria-label="add image"><MynauiImageRectangle></MynauiImageRectangle></button> -->
|
||||||
|
<Button description="附加圖片" onclick={add_image}
|
||||||
|
><MynauiImageRectangle></MynauiImageRectangle></Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ScrollArea orientation="horizontal">
|
||||||
|
<div class="flex w-max flex-row gap-3">
|
||||||
|
{#each image_to_url as map}
|
||||||
|
<img src={map[1]} class="h-52 rounded-md" alt={map[0]} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
{/if}
|
52
src/lib/Images.svelte
Normal file
52
src/lib/Images.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import axios from 'axios';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
let { hashIds }: { hashIds: Array<string> } = $props();
|
||||||
|
|
||||||
|
let filetype_url_map = new SvelteMap<string, string>();
|
||||||
|
|
||||||
|
let len = $state(0);
|
||||||
|
|
||||||
|
for (const hash of hashIds) {
|
||||||
|
axios.head(`/article/file/${hash}`).then((e) => {
|
||||||
|
const type = e.headers['content-type']?.toString();
|
||||||
|
if (type) {
|
||||||
|
filetype_url_map.set(`/article/file/${hash}`, type);
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const single = (): [string, string] => {
|
||||||
|
return filetype_url_map.entries().toArray()[0];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if len > 1}
|
||||||
|
<ScrollArea
|
||||||
|
class="my-auto h-fit w-fit max-w-[90%] whitespace-nowrap rounded-md border-0"
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<div class="flex h-36 w-max items-center space-x-4 lg:h-64">
|
||||||
|
{#each filetype_url_map as map}
|
||||||
|
{#if map[1].startsWith('image')}
|
||||||
|
<img src={map[0]} class="top-0 h-full object-contain" alt="Pic" />
|
||||||
|
{:else if map[1].startsWith('video')}
|
||||||
|
<video class="h-36 lg:h-64" id="player" playsinline controls>
|
||||||
|
<source src={map[0]} type={map[1]} />
|
||||||
|
</video>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
{:else if len == 1}
|
||||||
|
{#if single()[1].startsWith('image')}
|
||||||
|
<img src={single()[0]} class="h-36 w-fit rounded-md object-contain lg:h-64" alt="Pic" />
|
||||||
|
{:else if single()[1].startsWith('video')}
|
||||||
|
<video class="h-36 lg:h-64" src={single()[0]}></video>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
43
src/lib/Navigation.svelte
Normal file
43
src/lib/Navigation.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
<a href="/" class="left"><h1>匿名中工</h1> </a>
|
||||||
|
<ModeWatcher />
|
||||||
|
<a href="/tos">服務條款</a>
|
||||||
|
<a href="/" target="_blank">IG</a>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#container {
|
||||||
|
width: 100dvw;
|
||||||
|
height: 10dvh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0px 10px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
z-index: 50;
|
||||||
|
a {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
h1 {
|
||||||
|
margin: auto 0 auto;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
</style>
|
46
src/lib/Post.svelte
Normal file
46
src/lib/Post.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { FetchResponse_Message } from './protobuf/niming';
|
||||||
|
import { Dialog, ScrollArea } from 'bits-ui';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import Images from './Images.svelte';
|
||||||
|
let { post }: { post: FetchResponse_Message } = $props();
|
||||||
|
|
||||||
|
const url = /(https|http):\/\/[\S]*/g;
|
||||||
|
const preprocess = (text: string) => {
|
||||||
|
// Replace https:// or http:// text with <a href="">
|
||||||
|
text.replaceAll(url, (sub) => {
|
||||||
|
return `<a href=${sub}>${sub}</a>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return DOMPurify.sanitize(text);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger class="post_trigger mb-10 rounded-lg bg-card text-card-foreground">
|
||||||
|
<p class="h-fit max-h-[40dvh] overflow-y-hidden text-ellipsis text-pretty break-words">
|
||||||
|
{@html preprocess(post.content)}
|
||||||
|
</p>
|
||||||
|
<!-- <div class="pt-10"></div> -->
|
||||||
|
<Images hashIds={post.filesHash}></Images>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay transition={fade} transitionConfig={{ duration: 150 }} class="post_overlay" />
|
||||||
|
<Dialog.Content class="post_content h-fit min-h-[40dvh] w-[85dvw] rounded-lg lg:w-[50dvw]">
|
||||||
|
<Dialog.Close>Close</Dialog.Close>
|
||||||
|
<p class="h-fit text-pretty break-words">
|
||||||
|
{@html preprocess(post.content)}
|
||||||
|
</p>
|
||||||
|
<Images hashIds={post.filesHash}></Images>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#container {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
81
src/lib/api.ts
Normal file
81
src/lib/api.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { FetchResponse, Post, PostResponse } from '$lib/protobuf/niming';
|
||||||
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class Result<T> {
|
||||||
|
status: number
|
||||||
|
enclose?: T
|
||||||
|
message?: string
|
||||||
|
constructor(status: number, enclose?: T, message?: string) {
|
||||||
|
this.status = status;
|
||||||
|
this.enclose = enclose;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
ok(): boolean { return this.status == 1 }
|
||||||
|
set_status(status: number) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function axios_request(method: string, url: string, params?: Map<string, string> | Object, data?: Uint8Array): Promise<AxiosResponse<ArrayBuffer, any>> {
|
||||||
|
let cfg: AxiosRequestConfig<Uint8Array> = {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
params,
|
||||||
|
data,
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
headers: { 'Content-Type': 'application/x-protobuf' },
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class API {
|
||||||
|
page: number
|
||||||
|
constructor() {
|
||||||
|
this.page = 0
|
||||||
|
}
|
||||||
|
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
|
||||||
|
let files_buf: Uint8Array[] = [];
|
||||||
|
files?.forEach((e) => {
|
||||||
|
e.arrayBuffer().then((arrbuf) => {
|
||||||
|
files_buf.push(new Uint8Array(arrbuf));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let ctx = Post.create({ content: content, files: files_buf });
|
||||||
|
if (refer !== undefined) {
|
||||||
|
ctx.ref = refer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = new Result<PostResponse>(0)
|
||||||
|
|
||||||
|
await axios_request('post', '/article', undefined, Post.toBinary(ctx))
|
||||||
|
.then(e => {
|
||||||
|
result.enclose = PostResponse.fromBinary(new Uint8Array(e.data))
|
||||||
|
result.set_status(1);
|
||||||
|
}).catch(e => {
|
||||||
|
result.message = e.response?.data
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async fetch(): Promise<Result<FetchResponse>> {
|
||||||
|
let result = new Result<FetchResponse>(0)
|
||||||
|
|
||||||
|
await axios_request('get', '/article/list', { page: this.page })
|
||||||
|
.then(e => {
|
||||||
|
result.enclose = FetchResponse.fromBinary(new Uint8Array(e.data))
|
||||||
|
result.set_status(1);
|
||||||
|
this.page += 1;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
result.message = e.response?.data
|
||||||
|
})
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_inst = new API();
|
||||||
|
export default api_inst;
|
10
src/lib/components/ui/scroll-area/index.ts
Normal file
10
src/lib/components/ui/scroll-area/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Scrollbar from "./scroll-area-scrollbar.svelte";
|
||||||
|
import Root from "./scroll-area.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Scrollbar,
|
||||||
|
//,
|
||||||
|
Root as ScrollArea,
|
||||||
|
Scrollbar as ScrollAreaScrollbar,
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
|
||||||
|
orientation?: "vertical" | "horizontal";
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let orientation: $$Props["orientation"] = "vertical";
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
{orientation}
|
||||||
|
class={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
|
||||||
|
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ScrollAreaPrimitive.Thumb
|
||||||
|
class={cn("bg-border relative rounded-full", orientation === "vertical" && "flex-1")}
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
38
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
38
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
|
||||||
|
import { Scrollbar } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = ScrollAreaPrimitive.Props & {
|
||||||
|
orientation?: 'vertical' | 'horizontal' | 'both';
|
||||||
|
scrollbarXClasses?: string;
|
||||||
|
scrollbarYClasses?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
export let orientation = 'vertical';
|
||||||
|
export let scrollbarXClasses: string = '';
|
||||||
|
export let scrollbarYClasses: string = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>
|
||||||
|
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
|
||||||
|
{#if orientation === 'vertical'}
|
||||||
|
<ScrollAreaPrimitive.Content class="block w-full" style="">
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaPrimitive.Content>
|
||||||
|
{:else}
|
||||||
|
<ScrollAreaPrimitive.Content>
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaPrimitive.Content>
|
||||||
|
{/if}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
{#if orientation === 'vertical' || orientation === 'both'}
|
||||||
|
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||||
|
{/if}
|
||||||
|
{#if orientation === 'horizontal' || orientation === 'both'}
|
||||||
|
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||||
|
{/if}
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./separator.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Separator,
|
||||||
|
};
|
22
src/lib/components/ui/separator/separator.svelte
Normal file
22
src/lib/components/ui/separator/separator.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SeparatorPrimitive.Props;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let orientation: $$Props["orientation"] = "horizontal";
|
||||||
|
export let decorative: $$Props["decorative"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
class={cn(
|
||||||
|
"bg-border shrink-0",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{orientation}
|
||||||
|
{decorative}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
28
src/lib/components/ui/textarea/index.ts
Normal file
28
src/lib/components/ui/textarea/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Root from "./textarea.svelte";
|
||||||
|
|
||||||
|
type FormTextareaEvent<T extends Event = Event> = T & {
|
||||||
|
currentTarget: EventTarget & HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextareaEvents = {
|
||||||
|
blur: FormTextareaEvent<FocusEvent>;
|
||||||
|
change: FormTextareaEvent<Event>;
|
||||||
|
click: FormTextareaEvent<MouseEvent>;
|
||||||
|
focus: FormTextareaEvent<FocusEvent>;
|
||||||
|
keydown: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
keypress: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
keyup: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
mouseover: FormTextareaEvent<MouseEvent>;
|
||||||
|
mouseenter: FormTextareaEvent<MouseEvent>;
|
||||||
|
mouseleave: FormTextareaEvent<MouseEvent>;
|
||||||
|
paste: FormTextareaEvent<ClipboardEvent>;
|
||||||
|
input: FormTextareaEvent<InputEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Textarea,
|
||||||
|
type TextareaEvents,
|
||||||
|
type FormTextareaEvent,
|
||||||
|
};
|
38
src/lib/components/ui/textarea/textarea.svelte
Normal file
38
src/lib/components/ui/textarea/textarea.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||||
|
import type { TextareaEvents } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLTextareaAttributes;
|
||||||
|
type $$Events = TextareaEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||||
|
// Fixed in Svelte 5, but not backported to 4.x.
|
||||||
|
export let readonly: $$Props["readonly"] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{readonly}
|
||||||
|
on:blur
|
||||||
|
on:change
|
||||||
|
on:click
|
||||||
|
on:focus
|
||||||
|
on:keydown
|
||||||
|
on:keypress
|
||||||
|
on:keyup
|
||||||
|
on:mouseover
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
|
on:paste
|
||||||
|
on:input
|
||||||
|
{...$$restProps}
|
||||||
|
></textarea>
|
15
src/lib/components/ui/tooltip/index.ts
Normal file
15
src/lib/components/ui/tooltip/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||||
|
import Content from "./tooltip-content.svelte";
|
||||||
|
|
||||||
|
const Root = TooltipPrimitive.Root;
|
||||||
|
const Trigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
//
|
||||||
|
Root as Tooltip,
|
||||||
|
Content as TooltipContent,
|
||||||
|
Trigger as TooltipTrigger,
|
||||||
|
};
|
28
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal file
28
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||||
|
import { cn, flyAndScale } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = TooltipPrimitive.ContentProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||||
|
export let transition: $$Props["transition"] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||||
|
y: 8,
|
||||||
|
duration: 150,
|
||||||
|
};
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TooltipPrimitive.Content>
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
38
src/lib/protobuf/definitions/niming.proto
Normal file
38
src/lib/protobuf/definitions/niming.proto
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// This is for posting a paragraph.
|
||||||
|
message Post {
|
||||||
|
string content = 1;
|
||||||
|
// reply to a post, like a mail chat.
|
||||||
|
optional int64 ref = 2;
|
||||||
|
repeated bytes files = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Failed = 0;
|
||||||
|
Success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response of the posting, defining what should return.
|
||||||
|
message PostResponse {
|
||||||
|
Status status = 1;
|
||||||
|
string hash = 2;
|
||||||
|
uint64 id = 3;
|
||||||
|
optional string failed_message = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FetchResponse {
|
||||||
|
message Message {
|
||||||
|
uint64 id = 1;
|
||||||
|
string content = 2;
|
||||||
|
// reply to a post, like a mail chat.
|
||||||
|
// optional uint64 ref = 3;
|
||||||
|
// request files through /article/file/<id> with MIME type.
|
||||||
|
// See it as a BLOB url;
|
||||||
|
repeated string files_hash = 3;
|
||||||
|
optional string igid = 4;
|
||||||
|
repeated string comments_hash = 5;
|
||||||
|
}
|
||||||
|
// Several post info
|
||||||
|
repeated Message posts = 1;
|
||||||
|
}
|
367
src/lib/protobuf/niming.ts
Normal file
367
src/lib/protobuf/niming.ts
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
// @generated by protobuf-ts 2.9.4
|
||||||
|
// @generated from protobuf file "niming.proto" (syntax proto3)
|
||||||
|
// tslint:disable
|
||||||
|
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryWriter } from "@protobuf-ts/runtime";
|
||||||
|
import { WireType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryReader } from "@protobuf-ts/runtime";
|
||||||
|
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||||
|
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||||
|
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||||
|
import { MessageType } from "@protobuf-ts/runtime";
|
||||||
|
/**
|
||||||
|
* This is for posting a paragraph.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message Post
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string content = 1;
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
/**
|
||||||
|
* reply to a post, like a mail chat.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: optional int64 ref = 2;
|
||||||
|
*/
|
||||||
|
ref?: bigint;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: repeated bytes files = 3;
|
||||||
|
*/
|
||||||
|
files: Uint8Array[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The response of the posting, defining what should return.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message PostResponse
|
||||||
|
*/
|
||||||
|
export interface PostResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: Status status = 1;
|
||||||
|
*/
|
||||||
|
status: Status;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string hash = 2;
|
||||||
|
*/
|
||||||
|
hash: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 id = 3;
|
||||||
|
*/
|
||||||
|
id: bigint;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: optional string failed_message = 4;
|
||||||
|
*/
|
||||||
|
failedMessage?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message FetchResponse
|
||||||
|
*/
|
||||||
|
export interface FetchResponse {
|
||||||
|
/**
|
||||||
|
* Several post info
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: repeated FetchResponse.Message posts = 1;
|
||||||
|
*/
|
||||||
|
posts: FetchResponse_Message[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message FetchResponse.Message
|
||||||
|
*/
|
||||||
|
export interface FetchResponse_Message {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 id = 1;
|
||||||
|
*/
|
||||||
|
id: bigint;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string content = 2;
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
/**
|
||||||
|
* reply to a post, like a mail chat.
|
||||||
|
* optional uint64 ref = 3;
|
||||||
|
* request files through /article/file/<id> with MIME type.
|
||||||
|
* See it as a BLOB url;
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: repeated string files_hash = 3;
|
||||||
|
*/
|
||||||
|
filesHash: string[];
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: optional string igid = 4;
|
||||||
|
*/
|
||||||
|
igid?: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: repeated string comments_hash = 5;
|
||||||
|
*/
|
||||||
|
commentsHash: string[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf enum Status
|
||||||
|
*/
|
||||||
|
export enum Status {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf enum value: Failed = 0;
|
||||||
|
*/
|
||||||
|
Failed = 0,
|
||||||
|
/**
|
||||||
|
* @generated from protobuf enum value: Success = 1;
|
||||||
|
*/
|
||||||
|
Success = 1
|
||||||
|
}
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Post$Type extends MessageType<Post> {
|
||||||
|
constructor() {
|
||||||
|
super("Post", [
|
||||||
|
{ no: 1, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 2, name: "ref", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
|
||||||
|
{ no: 3, name: "files", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 12 /*ScalarType.BYTES*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Post>): Post {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.content = "";
|
||||||
|
message.files = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Post>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Post): Post {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string content */ 1:
|
||||||
|
message.content = reader.string();
|
||||||
|
break;
|
||||||
|
case /* optional int64 ref */ 2:
|
||||||
|
message.ref = reader.int64().toBigInt();
|
||||||
|
break;
|
||||||
|
case /* repeated bytes files */ 3:
|
||||||
|
message.files.push(reader.bytes());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: Post, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* string content = 1; */
|
||||||
|
if (message.content !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.content);
|
||||||
|
/* optional int64 ref = 2; */
|
||||||
|
if (message.ref !== undefined)
|
||||||
|
writer.tag(2, WireType.Varint).int64(message.ref);
|
||||||
|
/* repeated bytes files = 3; */
|
||||||
|
for (let i = 0; i < message.files.length; i++)
|
||||||
|
writer.tag(3, WireType.LengthDelimited).bytes(message.files[i]);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message Post
|
||||||
|
*/
|
||||||
|
export const Post = new Post$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class PostResponse$Type extends MessageType<PostResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("PostResponse", [
|
||||||
|
{ no: 1, name: "status", kind: "enum", T: () => ["Status", Status] },
|
||||||
|
{ no: 2, name: "hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
|
||||||
|
{ no: 4, name: "failed_message", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<PostResponse>): PostResponse {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.status = 0;
|
||||||
|
message.hash = "";
|
||||||
|
message.id = 0n;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<PostResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PostResponse): PostResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* Status status */ 1:
|
||||||
|
message.status = reader.int32();
|
||||||
|
break;
|
||||||
|
case /* string hash */ 2:
|
||||||
|
message.hash = reader.string();
|
||||||
|
break;
|
||||||
|
case /* uint64 id */ 3:
|
||||||
|
message.id = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
case /* optional string failed_message */ 4:
|
||||||
|
message.failedMessage = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* Status status = 1; */
|
||||||
|
if (message.status !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).int32(message.status);
|
||||||
|
/* string hash = 2; */
|
||||||
|
if (message.hash !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.hash);
|
||||||
|
/* uint64 id = 3; */
|
||||||
|
if (message.id !== 0n)
|
||||||
|
writer.tag(3, WireType.Varint).uint64(message.id);
|
||||||
|
/* optional string failed_message = 4; */
|
||||||
|
if (message.failedMessage !== undefined)
|
||||||
|
writer.tag(4, WireType.LengthDelimited).string(message.failedMessage);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message PostResponse
|
||||||
|
*/
|
||||||
|
export const PostResponse = new PostResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FetchResponse$Type extends MessageType<FetchResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("FetchResponse", [
|
||||||
|
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FetchResponse_Message }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FetchResponse>): FetchResponse {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.posts = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FetchResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse): FetchResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* repeated FetchResponse.Message posts */ 1:
|
||||||
|
message.posts.push(FetchResponse_Message.internalBinaryRead(reader, reader.uint32(), options));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: FetchResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* repeated FetchResponse.Message posts = 1; */
|
||||||
|
for (let i = 0; i < message.posts.length; i++)
|
||||||
|
FetchResponse_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message FetchResponse
|
||||||
|
*/
|
||||||
|
export const FetchResponse = new FetchResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FetchResponse_Message$Type extends MessageType<FetchResponse_Message> {
|
||||||
|
constructor() {
|
||||||
|
super("FetchResponse.Message", [
|
||||||
|
{ no: 1, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
|
||||||
|
{ no: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "files_hash", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 4, name: "igid", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 5, name: "comments_hash", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FetchResponse_Message>): FetchResponse_Message {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.id = 0n;
|
||||||
|
message.content = "";
|
||||||
|
message.filesHash = [];
|
||||||
|
message.commentsHash = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FetchResponse_Message>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse_Message): FetchResponse_Message {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint64 id */ 1:
|
||||||
|
message.id = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
case /* string content */ 2:
|
||||||
|
message.content = reader.string();
|
||||||
|
break;
|
||||||
|
case /* repeated string files_hash */ 3:
|
||||||
|
message.filesHash.push(reader.string());
|
||||||
|
break;
|
||||||
|
case /* optional string igid */ 4:
|
||||||
|
message.igid = reader.string();
|
||||||
|
break;
|
||||||
|
case /* repeated string comments_hash */ 5:
|
||||||
|
message.commentsHash.push(reader.string());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: FetchResponse_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint64 id = 1; */
|
||||||
|
if (message.id !== 0n)
|
||||||
|
writer.tag(1, WireType.Varint).uint64(message.id);
|
||||||
|
/* string content = 2; */
|
||||||
|
if (message.content !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.content);
|
||||||
|
/* repeated string files_hash = 3; */
|
||||||
|
for (let i = 0; i < message.filesHash.length; i++)
|
||||||
|
writer.tag(3, WireType.LengthDelimited).string(message.filesHash[i]);
|
||||||
|
/* optional string igid = 4; */
|
||||||
|
if (message.igid !== undefined)
|
||||||
|
writer.tag(4, WireType.LengthDelimited).string(message.igid);
|
||||||
|
/* repeated string comments_hash = 5; */
|
||||||
|
for (let i = 0; i < message.commentsHash.length; i++)
|
||||||
|
writer.tag(5, WireType.LengthDelimited).string(message.commentsHash[i]);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message FetchResponse.Message
|
||||||
|
*/
|
||||||
|
export const FetchResponse_Message = new FetchResponse_Message$Type();
|
62
src/lib/utils.ts
Normal file
62
src/lib/utils.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { cubicOut } from "svelte/easing";
|
||||||
|
import type { TransitionConfig } from "svelte/transition";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlyAndScaleParams = {
|
||||||
|
y?: number;
|
||||||
|
x?: number;
|
||||||
|
start?: number;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flyAndScale = (
|
||||||
|
node: Element,
|
||||||
|
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||||
|
): TransitionConfig => {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
const transform = style.transform === "none" ? "" : style.transform;
|
||||||
|
|
||||||
|
const scaleConversion = (
|
||||||
|
valueA: number,
|
||||||
|
scaleA: [number, number],
|
||||||
|
scaleB: [number, number]
|
||||||
|
) => {
|
||||||
|
const [minA, maxA] = scaleA;
|
||||||
|
const [minB, maxB] = scaleB;
|
||||||
|
|
||||||
|
const percentage = (valueA - minA) / (maxA - minA);
|
||||||
|
const valueB = percentage * (maxB - minB) + minB;
|
||||||
|
|
||||||
|
return valueB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleToString = (
|
||||||
|
style: Record<string, number | string | undefined>
|
||||||
|
): string => {
|
||||||
|
return Object.keys(style).reduce((str, key) => {
|
||||||
|
if (style[key] === undefined) return str;
|
||||||
|
return str + `${key}:${style[key]};`;
|
||||||
|
}, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: params.duration ?? 200,
|
||||||
|
delay: 0,
|
||||||
|
css: (t) => {
|
||||||
|
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||||
|
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||||
|
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||||
|
|
||||||
|
return styleToString({
|
||||||
|
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||||
|
opacity: t
|
||||||
|
});
|
||||||
|
},
|
||||||
|
easing: cubicOut
|
||||||
|
};
|
||||||
|
};
|
13
src/routes/+error.svelte
Normal file
13
src/routes/+error.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div id="container">
|
||||||
|
<h1>虛空之境,杳無人煙</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
#container {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
6
src/routes/+layout.svelte
Normal file
6
src/routes/+layout.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
let { children } = $props();
|
||||||
|
import '../app.css';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
76
src/routes/+page.svelte
Normal file
76
src/routes/+page.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Navigation from '$lib/Navigation.svelte';
|
||||||
|
import api_inst from '$lib/api';
|
||||||
|
import { FetchResponse_Message } from '$lib/protobuf/niming';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
import Post from '$lib/Post.svelte';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
import Editor from '$lib/Editor.svelte';
|
||||||
|
|
||||||
|
let posts = new SvelteSet<FetchResponse_Message>();
|
||||||
|
let width = $state(0);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let trigger = document.querySelector('#trigger');
|
||||||
|
let observe = new IntersectionObserver(
|
||||||
|
() => {
|
||||||
|
api_inst.fetch().then((e) => {
|
||||||
|
if (e.enclose?.posts !== undefined) {
|
||||||
|
for (const i of e.enclose?.posts) {
|
||||||
|
posts.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ root: null, rootMargin: '0px', threshold: 1.0 }
|
||||||
|
);
|
||||||
|
if (trigger) {
|
||||||
|
observe.observe(trigger);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:body bind:clientWidth={width} />
|
||||||
|
|
||||||
|
<Navigation><div></div></Navigation>
|
||||||
|
<!-- Posts -->
|
||||||
|
<div id="container" class:layout1={width >= 950} class:layout2={width < 950}>
|
||||||
|
{#if width >= 950}
|
||||||
|
<ScrollArea class="h-full w-full pl-[3%] pr-[7.5%]" orientation="vertical">
|
||||||
|
<div class="flex h-max flex-col">
|
||||||
|
{#each posts as post}
|
||||||
|
<Post {post} />
|
||||||
|
{/each}
|
||||||
|
<p id="trigger"></p>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<Editor mode="wide" />
|
||||||
|
{:else}
|
||||||
|
<ScrollArea class="h-full w-full pl-[3%] pr-[3%]" orientation="vertical">
|
||||||
|
<div class="flex h-max flex-col">
|
||||||
|
{#each posts as post}
|
||||||
|
<Post {post} />
|
||||||
|
{/each}
|
||||||
|
<p id="trigger"></p>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<Editor mode="compact" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#container {
|
||||||
|
width: 100dvw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 90dvh;
|
||||||
|
}
|
||||||
|
.layout1 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 8fr 4fr;
|
||||||
|
}
|
||||||
|
.layout2 {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
11
src/routes/tos/+page.svelte
Normal file
11
src/routes/tos/+page.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
import Separator from '$lib/components/ui/separator/separator.svelte';
|
||||||
|
import Navigation from '$lib/Navigation.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navigation><div></div></Navigation>
|
||||||
|
<div class="h-fit w-dvw px-[15%] text-center">
|
||||||
|
<h1 class="text-4xl font-bold">服務條款</h1>
|
||||||
|
<Separator class="my-8 text-foreground"></Separator>
|
||||||
|
<ol>Termainl Is the Best Stuff</ol>
|
||||||
|
</div>
|
3
src/vite-env.d.ts
vendored
Normal file
3
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="unplugin-icons/types/svelte" />
|
BIN
static/PingFang Bold.woff2
Normal file
BIN
static/PingFang Bold.woff2
Normal file
Binary file not shown.
BIN
static/PingFang Heavy.woff2
Normal file
BIN
static/PingFang Heavy.woff2
Normal file
Binary file not shown.
BIN
static/PingFang Light.woff2
Normal file
BIN
static/PingFang Light.woff2
Normal file
Binary file not shown.
BIN
static/PingFang Medium.woff2
Normal file
BIN
static/PingFang Medium.woff2
Normal file
Binary file not shown.
BIN
static/PingFang Regular.woff2
Normal file
BIN
static/PingFang Regular.woff2
Normal file
Binary file not shown.
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter({ pages: 'build', assets: 'build', preprocess: false, strict: true })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
61
tailwind.config.ts
Normal file
61
tailwind.config.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: "selector",
|
||||||
|
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||||
|
safelist: ["dark"],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border) / <alpha-value>)",
|
||||||
|
input: "hsl(var(--input) / <alpha-value>)",
|
||||||
|
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||||
|
background: "hsl(var(--background) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit(), Icons({
|
||||||
|
compiler: 'svelte',
|
||||||
|
})],
|
||||||
|
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/article': {
|
||||||
|
target: "http://10.16.20.17:5000",
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user