This commit is contained in:
jasinco 2024-12-22 22:57:01 +08:00
parent 0a754c17a3
commit 5eb5e81f3f
23 changed files with 667 additions and 396 deletions

5
.gitignore vendored
View File

@ -19,3 +19,8 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# lock files
bun.lockb
yarn.lock
.yarn/

View File

@ -17,7 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/dompurify": "^3",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"bits-ui": "^0.22.0",
"clsx": "^2.1.1",
"lucide-svelte": "^0.454.0",
"prettier": "^3.3.2",

View File

@ -125,6 +125,8 @@ body {
font-family: "PingFang";
font-weight: 400;
overflow-x: hidden;
width: 100dvw;
min-height: 100dvh;
}
::-webkit-scrollbar {

View File

@ -1,17 +1,35 @@
import { Fetch, Post, PostResponse } from '$lib/protobuf/niming';
import axios from 'axios';
import { FetchResponse, Post, PostResponse } from '$lib/protobuf/niming';
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
export const ResultEnum = {
Ok: 1,
Err: 2
} as const;
export type ResultEnum = typeof ResultEnum[keyof typeof ResultEnum];
export interface Result<T> {
kind: ResultEnum;
message: string
enclose?: T;
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 {
@ -20,42 +38,42 @@ export class API {
this.page = 0
}
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
let readed: Uint8Array[] = [];
let files_buf: Uint8Array[] = [];
files?.forEach((e) => {
e.arrayBuffer().then((arrbuf) => {
readed.push(new Uint8Array(arrbuf));
files_buf.push(new Uint8Array(arrbuf));
});
});
let ctx = Post.create({ content: content, files: readed });
let ctx = Post.create({ content: content, files: files_buf });
if (refer !== undefined) {
ctx.ref = refer;
}
let result: Result<PostResponse> = { kind: ResultEnum.Err, message: "" };
await axios
.post('/article/', Post.toBinary(ctx), {
headers: { 'Content-Type': 'application/x-protobuf' },
responseType: "arraybuffer"
})
.then((e) => {
result.kind = ResultEnum.Ok;
result.enclose = PostResponse.fromBinary(e.data);
})
.catch((err) => {
if (err.response) {
result.message = err.response.data.error;
}
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() {
let result: Result<Fetch> = { kind: ResultEnum.Err, message: "" };
axios.get("/article/list", { responseType: "arraybuffer" }).then((e) => {
result.kind = ResultEnum.Ok;
result.enclose = Fetch.fromBinary(e.data);
}).catch((e) => {
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
})
});
this.page++;
return result;
}
}

View File

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View File

@ -0,0 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View 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,
};

View File

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

View File

@ -0,0 +1,32 @@
<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]">
<ScrollAreaPrimitive.Content>
<slot />
</ScrollAreaPrimitive.Content>
</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>

View 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,
};

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

View File

@ -1,2 +1 @@
// place files you want to import through the `$lib` alias in this folder.
export * from './textmark'

View File

@ -8,21 +8,30 @@ message Post {
repeated bytes files = 3;
}
// The response of the posting, defining what should return.
message PostResponse {
string hash = 1;
uint64 id = 2;
enum Status {
Failed = 0;
Success = 1;
}
message Fetch {
// 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;
// optional uint64 ref = 3;
// request files through /article/file/<id> with MIME type.
// See it as a BLOB url;
repeated uint64 files_id = 4;
repeated string files_hash = 3;
optional string igid = 4;
repeated string comments_hash = 5;
}
// Several post info
repeated Message posts = 1;

View File

@ -38,29 +38,37 @@ export interface Post {
*/
export interface PostResponse {
/**
* @generated from protobuf field: string hash = 1;
* @generated from protobuf field: Status status = 1;
*/
status: Status;
/**
* @generated from protobuf field: string hash = 2;
*/
hash: string;
/**
* @generated from protobuf field: uint64 id = 2;
* @generated from protobuf field: uint64 id = 3;
*/
id: bigint;
/**
* @generated from protobuf field: optional string failed_message = 4;
*/
failedMessage?: string;
}
/**
* @generated from protobuf message Fetch
* @generated from protobuf message FetchResponse
*/
export interface Fetch {
export interface FetchResponse {
/**
* Several post info
*
* @generated from protobuf field: repeated Fetch.Message posts = 1;
* @generated from protobuf field: repeated FetchResponse.Message posts = 1;
*/
posts: Fetch_Message[];
posts: FetchResponse_Message[];
}
/**
* @generated from protobuf message Fetch.Message
* @generated from protobuf message FetchResponse.Message
*/
export interface Fetch_Message {
export interface FetchResponse_Message {
/**
* @generated from protobuf field: uint64 id = 1;
*/
@ -71,17 +79,34 @@ export interface Fetch_Message {
content: string;
/**
* reply to a post, like a mail chat.
*
* @generated from protobuf field: optional uint64 ref = 3;
*/
ref?: bigint;
/**
* optional uint64 ref = 3;
* request files through /article/file/<id> with MIME type.
* See it as a BLOB url;
*
* @generated from protobuf field: repeated uint64 files_id = 4;
* @generated from protobuf field: repeated string files_hash = 3;
*/
filesId: bigint[];
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> {
@ -149,12 +174,15 @@ export const Post = new Post$Type();
class PostResponse$Type extends MessageType<PostResponse> {
constructor() {
super("PostResponse", [
{ no: 1, name: "hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
{ 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)
@ -166,12 +194,18 @@ class PostResponse$Type extends MessageType<PostResponse> {
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string hash */ 1:
case /* Status status */ 1:
message.status = reader.int32();
break;
case /* string hash */ 2:
message.hash = reader.string();
break;
case /* uint64 id */ 2:
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")
@ -184,12 +218,18 @@ class PostResponse$Type extends MessageType<PostResponse> {
return message;
}
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string hash = 1; */
/* Status status = 1; */
if (message.status !== 0)
writer.tag(1, WireType.Varint).int32(message.status);
/* string hash = 2; */
if (message.hash !== "")
writer.tag(1, WireType.LengthDelimited).string(message.hash);
/* uint64 id = 2; */
writer.tag(2, WireType.LengthDelimited).string(message.hash);
/* uint64 id = 3; */
if (message.id !== 0n)
writer.tag(2, WireType.Varint).uint64(message.id);
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);
@ -201,26 +241,26 @@ class PostResponse$Type extends MessageType<PostResponse> {
*/
export const PostResponse = new PostResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Fetch$Type extends MessageType<Fetch> {
class FetchResponse$Type extends MessageType<FetchResponse> {
constructor() {
super("Fetch", [
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Fetch_Message }
super("FetchResponse", [
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FetchResponse_Message }
]);
}
create(value?: PartialMessage<Fetch>): Fetch {
create(value?: PartialMessage<FetchResponse>): FetchResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.posts = [];
if (value !== undefined)
reflectionMergePartial<Fetch>(this, message, value);
reflectionMergePartial<FetchResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch): Fetch {
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 Fetch.Message posts */ 1:
message.posts.push(Fetch_Message.internalBinaryRead(reader, reader.uint32(), options));
case /* repeated FetchResponse.Message posts */ 1:
message.posts.push(FetchResponse_Message.internalBinaryRead(reader, reader.uint32(), options));
break;
default:
let u = options.readUnknownField;
@ -233,10 +273,10 @@ class Fetch$Type extends MessageType<Fetch> {
}
return message;
}
internalBinaryWrite(message: Fetch, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated Fetch.Message posts = 1; */
internalBinaryWrite(message: FetchResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated FetchResponse.Message posts = 1; */
for (let i = 0; i < message.posts.length; i++)
Fetch_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
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);
@ -244,29 +284,31 @@ class Fetch$Type extends MessageType<Fetch> {
}
}
/**
* @generated MessageType for protobuf message Fetch
* @generated MessageType for protobuf message FetchResponse
*/
export const Fetch = new Fetch$Type();
export const FetchResponse = new FetchResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Fetch_Message$Type extends MessageType<Fetch_Message> {
class FetchResponse_Message$Type extends MessageType<FetchResponse_Message> {
constructor() {
super("Fetch.Message", [
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: "ref", kind: "scalar", opt: true, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 4, name: "files_id", kind: "scalar", repeat: 1 /*RepeatType.PACKED*/, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
{ 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<Fetch_Message>): Fetch_Message {
create(value?: PartialMessage<FetchResponse_Message>): FetchResponse_Message {
const message = globalThis.Object.create((this.messagePrototype!));
message.id = 0n;
message.content = "";
message.filesId = [];
message.filesHash = [];
message.commentsHash = [];
if (value !== undefined)
reflectionMergePartial<Fetch_Message>(this, message, value);
reflectionMergePartial<FetchResponse_Message>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch_Message): Fetch_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();
@ -277,15 +319,14 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
case /* string content */ 2:
message.content = reader.string();
break;
case /* optional uint64 ref */ 3:
message.ref = reader.uint64().toBigInt();
case /* repeated string files_hash */ 3:
message.filesHash.push(reader.string());
break;
case /* repeated uint64 files_id */ 4:
if (wireType === WireType.LengthDelimited)
for (let e = reader.int32() + reader.pos; reader.pos < e;)
message.filesId.push(reader.uint64().toBigInt());
else
message.filesId.push(reader.uint64().toBigInt());
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;
@ -298,23 +339,22 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
}
return message;
}
internalBinaryWrite(message: Fetch_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
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);
/* optional uint64 ref = 3; */
if (message.ref !== undefined)
writer.tag(3, WireType.Varint).uint64(message.ref);
/* repeated uint64 files_id = 4; */
if (message.filesId.length) {
writer.tag(4, WireType.LengthDelimited).fork();
for (let i = 0; i < message.filesId.length; i++)
writer.uint64(message.filesId[i]);
writer.join();
}
/* 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);
@ -322,6 +362,6 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
}
}
/**
* @generated MessageType for protobuf message Fetch.Message
* @generated MessageType for protobuf message FetchResponse.Message
*/
export const Fetch_Message = new Fetch_Message$Type();
export const FetchResponse_Message = new FetchResponse_Message$Type();

View File

@ -1,43 +0,0 @@
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
import DOMPurify from "isomorphic-dompurify";
export default class TextMark {
marked: Marked
constructor() {
this.marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, _) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
this.marked = this.marked.use({
async: true,
gfm: true,
pedantic: false,
hooks: {
postprocess(e) {
return DOMPurify.sanitize(e)
}
}
})
}
async parse(pure_mark: boolean, content: string): Promise<string> {
if (!pure_mark) {
let processed = "";
content.split("\n").forEach((e) => {
processed += e + " \n"
})
content = processed;
}
return this.marked.parse(content);
}
}

View File

@ -3,7 +3,7 @@
let { children } = $props();
</script>
<div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
<div class="sticky top-0 z-50 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
<div class="col-span-3 grid grid-cols-4 text-4xl">
<a class="m-auto" href="/">中工匿名</a>
</div>

View File

@ -1,20 +1,25 @@
<script lang="ts">
import MessageCard from './message_card.svelte';
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte';
import Marker, { get_content, files, clear_content } from './marker.svelte';
import api_inst, { ResultEnum, type Result } from '$lib/api';
import api_inst, { type Result } from '$lib/api';
import { FetchResponse_Message, type PostResponse } from '$lib/protobuf/niming';
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import { Dialog } from 'bits-ui';
import { fade } from 'svelte/transition';
let timer = false;
let open = $state(false);
const send = () => {
if (!timer) {
timer = true;
api_inst.send_post(get_content(), undefined, files).then((result: Result<null>) => {
api_inst.send_post(get_content(), undefined, files).then((result: Result<PostResponse>) => {
alert(result.message);
if (result.kind == ResultEnum.Ok) {
if (result.ok()) {
clear_content();
toggle_overlay();
open = false;
}
setTimeout(() => {
timer = false;
@ -22,40 +27,64 @@
});
}
};
let posts = new SvelteSet<FetchResponse_Message>();
onMount(() => {
api_inst.page = 0;
let observe = new IntersectionObserver(
(_) => {
console.log('Intersecting');
api_inst.fetch().then((e) => {
console.log(api_inst.page);
if (e.ok() && e.enclose != undefined) {
e.enclose?.posts.forEach((post) => {
posts.add(post);
});
}
});
},
{
root: null,
rootMargin: '0px',
threshold: 1
}
);
const element = document.querySelector('#loader');
if (element != null) {
observe.observe(element);
}
});
$inspect(posts);
</script>
<div style="width:100dvw;" class="grid h-dvh px-5 py-3">
<!-- <p class="justify-self-center m-auto">空空如也...</p> -->
<MessageCard
content="
# H1
## H2
### H3
[url](https://google.com)
![image](http://localhost:5173/favicon.png)
Test
NormalText
"
/>
<div
class="relative grid h-fit min-h-dvh w-dvw grid-cols-1 justify-items-center gap-10 py-3 lg:grid-cols-2 lg:px-[10dvw]"
>
{#each posts as p}
<MessageCard post={p}></MessageCard>
{/each}
<div class="absolute bottom-0 left-0 h-10 w-full text-center" id="loader">Loading...</div>
</div>
<button id="add" onclick={toggle_overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button>
<Overlay>
<div
class="flex flex-col rounded-lg bg-primary px-5 py-3 md:w-4/6 lg:w-1/2"
style="height:60dvh;"
<Dialog.Root bind:open>
<Dialog.Trigger class="fixed bottom-[15dvh] right-[10dvw] text-4xl">
<MingcuteAddCircleFill />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
transition={fade}
transitionConfig={{ duration: 150 }}
class="fixed inset-0 z-50 bg-black/80"
/>
<Dialog.Content
class="fixed left-[50%] top-[50%] z-50 h-[40dvh] w-[50dvw] translate-x-[-50%] translate-y-[-50%]
rounded-lg border bg-background p-5 shadow-popover outline-none sm:w-[80dvw] md:w-[60dvw]"
>
<div class="relative h-6 w-full">
<button class="absolute left-0" onclick={toggle_overlay}>取消</button>
<button class="absolute right-0" onclickcapture={send}>發送</button>
<div class="grid grid-cols-3">
<Dialog.Close class="text-left">離開</Dialog.Close>
<Dialog.Title class="text-center">新增文章</Dialog.Title>
<button class="pr-1 text-right" onclickcapture={send}>Send</button>
</div>
<Marker />
</div>
</Overlay>
<style>
#add {
position: fixed;
bottom: calc(15dvh);
right: calc(10dvw);
}
</style>
<Marker></Marker>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@ -0,0 +1,3 @@
<div>
<p>IG: <a href="">IG 帳號</a></p>
</div>

View File

@ -5,13 +5,6 @@
let files: Array<File> = [];
const get_content = () => {
let length = dyn_files == undefined ? 0 : dyn_files.length;
for (let i = 0; i < length; i++) {
let item = dyn_files?.item(i);
if (item != null) {
files.push(item);
}
}
return content;
};
const clear_content = () => {
@ -24,9 +17,13 @@
<script lang="ts">
import { Textarea } from '$lib/components/ui/textarea';
import * as Tooltip from '$lib/components/ui/tooltip';
import type { KeyboardEventHandler } from 'svelte/elements';
import MingcutePicFill from '~icons/mingcute/pic-fill';
import MingcuteCloseCircleFill from '~icons/mingcute/close-circle-fill';
import { SvelteMap } from 'svelte/reactivity';
// It makes some of ARIA commands into inputs (like tab into \t)
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
if (event.key == 'Tab') {
@ -37,6 +34,26 @@
ct.selectionStart = ct.selectionEnd = start + 1;
}
};
// it use svelte's map in order to achieve reactivity(like $state)
// https://svelte.dev/docs/svelte/svelte-reactivity
let dyn_image_links = new SvelteMap<string, string>();
// it create a map of name and local files path to show images
const update_image = () => {
if (dyn_files != undefined) {
dyn_image_links.clear();
for (let i = 0; i < dyn_files.length; i++) {
let item = dyn_files.item(i);
if (item != null) {
dyn_image_links.set(item.name, URL.createObjectURL(item));
}
}
}
};
const delete_image = (key: string) => {
dyn_image_links.delete(key);
};
</script>
<div class="flex h-full w-full flex-col justify-items-center gap-3 pt-[3%]">
@ -48,6 +65,8 @@
/>
<hr />
<div class="w-full">
<Tooltip.Root>
<Tooltip.Trigger>
<input
id="enclosed"
type="file"
@ -55,9 +74,28 @@
multiple
class="hidden"
bind:files={dyn_files}
onchange={update_image}
/>
<label for="enclosed" class="mx-auto w-fit"><MingcutePicFill class="text-xl" /> </label>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Add a picture</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
<!-- selected images preview -->
{#if dyn_image_links.size > 0}
<div class="flex h-[75px] w-full max-w-full flex-row gap-3 overflow-y-hidden">
{#each dyn_image_links as entries}
<div class="relative h-[70px] w-[70px]">
<img src={entries[1]} alt="selected files" class="object-scale-down" />
<button class="absolute right-0 top-0" onclick={() => delete_image(entries[0])}>
<MingcuteCloseCircleFill />
</button>
</div>
{/each}
</div>
{/if}
</div>
<style>

View File

@ -1,53 +1,73 @@
<script lang="ts">
let { content }: { content: string } = $props();
let { post }: { post: FetchResponse_Message } = $props();
import * as Dialog from '$lib/components/ui/dialog';
import { Marked } from 'marked';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import type { FetchResponse_Message } from '$lib/protobuf/niming';
import DOMPurify from 'isomorphic-dompurify';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
//TODO Need sanitizer
const marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
marked.use({
hooks: {
postprocess: (text) => {
return DOMPurify.sanitize(text);
}
}
});
let parsed = marked.parse(content, {
async: true,
gfm: true,
pedantic: false
//Icons
import MingcuteForwardFill from '~icons/mingcute/forward-fill';
import MingcuteInsFill from '~icons/mingcute/ins-fill';
import MingcuteHeartFill from '~icons/mingcute/heart-fill';
const url = /(https|http):\/\/[\S]*/g;
const parser = (text: string) => {
text.replaceAll(url, (sub) => {
return `<a href=${sub}>${sub}</a>`;
});
return text;
};
let item = DOMPurify.sanitize(parser(post.content));
</script>
<Dialog.Root>
<div class="flex h-fit w-fit flex-row-reverse">
<div class="z-10 flex w-10 flex-col-reverse gap-3 text-2xl">
<!-- Interactions -->
<button class=" text-center" aria-label="like"><MingcuteHeartFill /></button>
<button
class=" text-center"
aria-label="ig"
onclick={() => {
window.open('', '_blank');
}}
>
<MingcuteInsFill />
</button>
<button class=" text-center" aria-label="forward"><MingcuteForwardFill /></button>
</div>
<Dialog.Trigger
class="max-h-80 max-w-64 overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f]"
class="h-fit max-h-80 max-w-[70dvw] overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f] lg:max-w-96"
>
<div class="inner_card">
{#await parsed}
loading
{:then item}
{@html item}
{/await}
{#if post.filesHash.length > 0}
<ScrollArea class="mt-10 w-full" orientation="horizontal">
{#each post.filesHash as hash}
<img src={'/article/file/' + hash} alt="pictures" />
{/each}
</ScrollArea>
{/if}
</div>
</Dialog.Trigger>
</div>
<Dialog.Content>
{#await parsed}
loading
{:then item}
{@html item}
{/await}
{#if post.filesHash.length > 1}
<ScrollArea class="w-full whitespace-nowrap rounded-md border" orientation="horizontal">
<div class="flex h-[35dvh] w-fit space-x-4 p-4">
{#each post.filesHash as hash}
<img src={'/article/file/' + hash} alt="pictures" class="aspect-auto object-cover" />
{/each}
</div>
</ScrollArea>
{:else if post.filesHash.length == 1}
<img
src={'/article/file/' + post.filesHash[0]}
alt="pictures"
class="aspect-auto object-cover"
/>
{/if}
</Dialog.Content>
</Dialog.Root>
@ -59,9 +79,9 @@
code {
@apply bg-amber-600;
}
mask-image: linear-gradient(0deg, transparent, white 50%);
mask-image: linear-gradient(0deg, transparent, white 30%);
height: 100%;
padding: 0px 5%;
padding: 5px 5% 40px;
text-align: left;
}
</style>

View File

@ -1,33 +0,0 @@
<script module lang="ts">
let overlay = $state(false);
const toggle_overlay = () => {
overlay = !overlay;
if (overlay) {
document.body.classList.add('overflow-y-hidden');
} else {
document.body.classList.remove('overflow-y-hidden');
}
};
const get_overlay = () => {
return overlay;
};
export { toggle_overlay, get_overlay };
</script>
<script lang="ts">
let { children } = $props();
</script>
<div
class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' +
(overlay ? 'overlay_blur grid place-items-center' : 'hidden')}
role="dialog"
>
{@render children()}
</div>
<style>
.overlay_blur {
backdrop-filter: blur(10px);
}
</style>

View File

@ -1,6 +1,4 @@
<div class="flex justify-center">
<div class="w-1/2 break-keep text-center text-2xl">
<p>“中工匿名”不記錄個人網路位置、名字。</p>
<div class="w-dvw text-center text-xl font-bold lg:text-3xl">
<p>“中工匿名”不記錄個人姓名。</p>
<p>如訊息侵害他人權利,違反大眾道德觀,皆不保證留存。</p>
</div>
</div>