diff --git a/justfile b/justfile new file mode 100644 index 0000000..d805661 --- /dev/null +++ b/justfile @@ -0,0 +1,3 @@ + +genproto: + npx protoc --ts_out ./src/lib/protobuf --proto_path ./src/lib/protobuf/definitions ./src/lib/protobuf/definitions/niming.proto diff --git a/package.json b/package.json index 6faf724..86979df 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "packageManager": "yarn@4.2.2+sha224.1e50daf19e5e249a025569752c60b88005fddf57d10fcde5fc68b88f", "dependencies": { "@iconify-json/mingcute": "^1.2.1", + "@protobuf-ts/plugin": "^2.9.4", + "axios": "^1.7.7", "dompurify": "^3.1.7", "highlight.js": "^11.10.0", "isomorphic-dompurify": "^2.16.0", diff --git a/src/app.css b/src/app.css index acd0e25..ca35437 100644 --- a/src/app.css +++ b/src/app.css @@ -34,6 +34,7 @@ --ring: 224 71.4% 4.1%; --radius: 0.5rem; + } .dark { @@ -49,7 +50,7 @@ --card: 35 88% 72%; --card-foreground: 248 13% 36%; - --border: 215 27.9% 16.9%; + --border: 248 13% 36%; --input: 215 27.9% 16.9%; --primary: 248deg 25% 18%; @@ -85,12 +86,56 @@ } @font-face { - font-family: "GenRyuMin2TW"; + font-family: "PingFang"; src: - url("/GenRyuMin2TW-M.woff2") format("woff2"); + 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; } body { - font-family: "GenRyuMin2TW"; + font-family: "PingFang"; + font-weight: 400; overflow-x: hidden; } + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background-color: hsl(var(--muted)) +} + +::-webkit-scrollbar-thumb { + background-color: hsl(248deg, 13%, 36%); + border-radius: 10px; +} diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..4cb0320 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,63 @@ +import { Fetch, Post, PostResponse } from '$lib/protobuf/niming'; +import axios from 'axios'; + +export const ResultEnum = { + Ok: 1, + Err: 2 +} as const; + +export type ResultEnum = typeof ResultEnum[keyof typeof ResultEnum]; + +export interface Result { + kind: ResultEnum; + message: string + enclose?: T; +} + +export class API { + page: number + constructor() { + this.page = 0 + } + async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) { + let readed: Uint8Array[] = []; + files?.forEach((e) => { + e.arrayBuffer().then((arrbuf) => { + readed.push(new Uint8Array(arrbuf)); + }); + }); + let ctx = Post.create({ content: content, files: readed }); + if (refer !== undefined) { + ctx.ref = refer; + } + let result: Result = { 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; + } + }); + return result; + } + async fetch() { + let result: Result = { kind: ResultEnum.Err, message: "" }; + axios.get("/article/list", { responseType: "arraybuffer" }).then((e) => { + result.kind = ResultEnum.Ok; + result.enclose = Fetch.fromBinary(e.data); + }).catch((e) => { + + }); + this.page++; + return result; + } +} +let api_inst = new API(); +export default api_inst; diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6..5022ad5 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,2 @@ // place files you want to import through the `$lib` alias in this folder. +export * from './textmark' diff --git a/src/lib/markdown.css b/src/lib/markdown.css new file mode 100644 index 0000000..fa9d201 --- /dev/null +++ b/src/lib/markdown.css @@ -0,0 +1 @@ +@import '$lib/hljs/rose-pine.css' diff --git a/src/lib/protobuf/definitions/niming.proto b/src/lib/protobuf/definitions/niming.proto new file mode 100644 index 0000000..893fde4 --- /dev/null +++ b/src/lib/protobuf/definitions/niming.proto @@ -0,0 +1,29 @@ +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; +} + +// The response of the posting, defining what should return. +message PostResponse { + string hash = 1; + uint64 id = 2; +} + +message Fetch { + 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/ with MIME type. + // See it as a BLOB url; + repeated uint64 files_id = 4; + } + // Several post info + repeated Message posts = 1; +} diff --git a/src/lib/protobuf/niming.ts b/src/lib/protobuf/niming.ts new file mode 100644 index 0000000..87f0591 --- /dev/null +++ b/src/lib/protobuf/niming.ts @@ -0,0 +1,327 @@ +// @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: string hash = 1; + */ + hash: string; + /** + * @generated from protobuf field: uint64 id = 2; + */ + id: bigint; +} +/** + * @generated from protobuf message Fetch + */ +export interface Fetch { + /** + * Several post info + * + * @generated from protobuf field: repeated Fetch.Message posts = 1; + */ + posts: Fetch_Message[]; +} +/** + * @generated from protobuf message Fetch.Message + */ +export interface Fetch_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. + * + * @generated from protobuf field: optional uint64 ref = 3; + */ + ref?: bigint; + /** + * request files through /article/file/ with MIME type. + * See it as a BLOB url; + * + * @generated from protobuf field: repeated uint64 files_id = 4; + */ + filesId: bigint[]; +} +// @generated message type with reflection information, may provide speed optimized methods +class Post$Type extends MessageType { + 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 { + const message = globalThis.Object.create((this.messagePrototype!)); + message.content = ""; + message.files = []; + if (value !== undefined) + reflectionMergePartial(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 { + 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*/ } + ]); + } + create(value?: PartialMessage): PostResponse { + const message = globalThis.Object.create((this.messagePrototype!)); + message.hash = ""; + message.id = 0n; + if (value !== undefined) + reflectionMergePartial(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 /* string hash */ 1: + message.hash = reader.string(); + break; + case /* uint64 id */ 2: + message.id = reader.uint64().toBigInt(); + 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 { + /* string hash = 1; */ + if (message.hash !== "") + writer.tag(1, WireType.LengthDelimited).string(message.hash); + /* uint64 id = 2; */ + if (message.id !== 0n) + writer.tag(2, WireType.Varint).uint64(message.id); + 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 Fetch$Type extends MessageType { + constructor() { + super("Fetch", [ + { no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Fetch_Message } + ]); + } + create(value?: PartialMessage): Fetch { + const message = globalThis.Object.create((this.messagePrototype!)); + message.posts = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch): Fetch { + 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)); + 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: Fetch, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* repeated Fetch.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(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message Fetch + */ +export const Fetch = new Fetch$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class Fetch_Message$Type extends MessageType { + constructor() { + super("Fetch.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*/ } + ]); + } + create(value?: PartialMessage): Fetch_Message { + const message = globalThis.Object.create((this.messagePrototype!)); + message.id = 0n; + message.content = ""; + message.filesId = []; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch_Message): Fetch_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 /* optional uint64 ref */ 3: + message.ref = reader.uint64().toBigInt(); + 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()); + 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: Fetch_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(); + } + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message Fetch.Message + */ +export const Fetch_Message = new Fetch_Message$Type(); diff --git a/src/lib/textmark.ts b/src/lib/textmark.ts new file mode 100644 index 0000000..8974bbc --- /dev/null +++ b/src/lib/textmark.ts @@ -0,0 +1,43 @@ +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 { + if (!pure_mark) { + let processed = ""; + content.split("\n").forEach((e) => { + processed += e + " \n" + }) + content = processed; + + } + return this.marked.parse(content); + } +} + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 11f9b3c..1b20a8f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,7 +3,7 @@ let { children } = $props(); -
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index aa0e357..5494105 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,16 +2,25 @@ import MessageCard from './message_card.svelte'; import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill'; import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte'; - import DOMPurify from 'isomorphic-dompurify'; - import Marker from './marker.svelte'; + import Marker, { get_content, files, clear_content } from './marker.svelte'; + import api_inst, { ResultEnum, type Result } from '$lib/api'; - let value: string; + let timer = false; - const overlay = () => { - if (!get_overlay()) { - } else { + const send = () => { + if (!timer) { + timer = true; + api_inst.send_post(get_content(), undefined, files).then((result: Result) => { + alert(result.message); + if (result.kind == ResultEnum.Ok) { + clear_content(); + toggle_overlay(); + } + setTimeout(() => { + timer = false; + }, 5000); + }); } - toggle_overlay(); }; @@ -29,14 +38,17 @@ NormalText " />
- + -
+
- - + +
- +
diff --git a/src/routes/marker.svelte b/src/routes/marker.svelte index 092a8b4..4efdef1 100644 --- a/src/routes/marker.svelte +++ b/src/routes/marker.svelte @@ -1,15 +1,32 @@ - + + - -
-
- {#if window_size < 360} -
- -
- {@html marked.parse(value).toString()} -
-
- {/if} +
+