Add Backend API
add backend api and add some element of the webpage
This commit is contained in:
parent
ec388f1b47
commit
0a754c17a3
3
justfile
Normal file
3
justfile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
genproto:
|
||||||
|
npx protoc --ts_out ./src/lib/protobuf --proto_path ./src/lib/protobuf/definitions ./src/lib/protobuf/definitions/niming.proto
|
@ -34,6 +34,8 @@
|
|||||||
"packageManager": "yarn@4.2.2+sha224.1e50daf19e5e249a025569752c60b88005fddf57d10fcde5fc68b88f",
|
"packageManager": "yarn@4.2.2+sha224.1e50daf19e5e249a025569752c60b88005fddf57d10fcde5fc68b88f",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify-json/mingcute": "^1.2.1",
|
"@iconify-json/mingcute": "^1.2.1",
|
||||||
|
"@protobuf-ts/plugin": "^2.9.4",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"dompurify": "^3.1.7",
|
"dompurify": "^3.1.7",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"isomorphic-dompurify": "^2.16.0",
|
"isomorphic-dompurify": "^2.16.0",
|
||||||
|
53
src/app.css
53
src/app.css
@ -34,6 +34,7 @@
|
|||||||
--ring: 224 71.4% 4.1%;
|
--ring: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -49,7 +50,7 @@
|
|||||||
--card: 35 88% 72%;
|
--card: 35 88% 72%;
|
||||||
--card-foreground: 248 13% 36%;
|
--card-foreground: 248 13% 36%;
|
||||||
|
|
||||||
--border: 215 27.9% 16.9%;
|
--border: 248 13% 36%;
|
||||||
--input: 215 27.9% 16.9%;
|
--input: 215 27.9% 16.9%;
|
||||||
|
|
||||||
--primary: 248deg 25% 18%;
|
--primary: 248deg 25% 18%;
|
||||||
@ -85,12 +86,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "GenRyuMin2TW";
|
font-family: "PingFang";
|
||||||
src:
|
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 {
|
body {
|
||||||
font-family: "GenRyuMin2TW";
|
font-family: "PingFang";
|
||||||
|
font-weight: 400;
|
||||||
overflow-x: hidden;
|
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;
|
||||||
|
}
|
||||||
|
63
src/lib/api.ts
Normal file
63
src/lib/api.ts
Normal file
@ -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<T> {
|
||||||
|
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<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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
this.page++;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_inst = new API();
|
||||||
|
export default api_inst;
|
@ -1 +1,2 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
|
export * from './textmark'
|
||||||
|
1
src/lib/markdown.css
Normal file
1
src/lib/markdown.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '$lib/hljs/rose-pine.css'
|
29
src/lib/protobuf/definitions/niming.proto
Normal file
29
src/lib/protobuf/definitions/niming.proto
Normal file
@ -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/<id> with MIME type.
|
||||||
|
// See it as a BLOB url;
|
||||||
|
repeated uint64 files_id = 4;
|
||||||
|
}
|
||||||
|
// Several post info
|
||||||
|
repeated Message posts = 1;
|
||||||
|
}
|
327
src/lib/protobuf/niming.ts
Normal file
327
src/lib/protobuf/niming.ts
Normal file
@ -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/<id> 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<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: "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>): PostResponse {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
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 /* 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<Fetch> {
|
||||||
|
constructor() {
|
||||||
|
super("Fetch", [
|
||||||
|
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Fetch_Message }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Fetch>): Fetch {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.posts = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Fetch>(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<Fetch_Message> {
|
||||||
|
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>): Fetch_Message {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.id = 0n;
|
||||||
|
message.content = "";
|
||||||
|
message.filesId = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Fetch_Message>(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();
|
43
src/lib/textmark.ts
Normal file
43
src/lib/textmark.ts
Normal file
@ -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<string> {
|
||||||
|
if (!pure_mark) {
|
||||||
|
let processed = "";
|
||||||
|
content.split("\n").forEach((e) => {
|
||||||
|
processed += e + " \n"
|
||||||
|
})
|
||||||
|
content = processed;
|
||||||
|
|
||||||
|
}
|
||||||
|
return this.marked.parse(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 bg-background">
|
<div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
|
||||||
<div class="col-span-3 grid grid-cols-4 text-4xl">
|
<div class="col-span-3 grid grid-cols-4 text-4xl">
|
||||||
<a class="m-auto" href="/">中工匿名</a>
|
<a class="m-auto" href="/">中工匿名</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,16 +2,25 @@
|
|||||||
import MessageCard from './message_card.svelte';
|
import MessageCard from './message_card.svelte';
|
||||||
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
|
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
|
||||||
import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte';
|
import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import Marker, { get_content, files, clear_content } from './marker.svelte';
|
||||||
import Marker from './marker.svelte';
|
import api_inst, { ResultEnum, type Result } from '$lib/api';
|
||||||
|
|
||||||
let value: string;
|
let timer = false;
|
||||||
|
|
||||||
const overlay = () => {
|
const send = () => {
|
||||||
if (!get_overlay()) {
|
if (!timer) {
|
||||||
} else {
|
timer = true;
|
||||||
}
|
api_inst.send_post(get_content(), undefined, files).then((result: Result<null>) => {
|
||||||
|
alert(result.message);
|
||||||
|
if (result.kind == ResultEnum.Ok) {
|
||||||
|
clear_content();
|
||||||
toggle_overlay();
|
toggle_overlay();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
timer = false;
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -29,14 +38,17 @@ NormalText
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button id="add" onclick={overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button>
|
<button id="add" onclick={toggle_overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button>
|
||||||
<Overlay>
|
<Overlay>
|
||||||
<div class="flex w-4/6 flex-col rounded-lg bg-primary px-5 py-3" style="height:60dvh;">
|
<div
|
||||||
|
class="flex flex-col rounded-lg bg-primary px-5 py-3 md:w-4/6 lg:w-1/2"
|
||||||
|
style="height:60dvh;"
|
||||||
|
>
|
||||||
<div class="relative h-6 w-full">
|
<div class="relative h-6 w-full">
|
||||||
<button class="absolute left-0" onclick={overlay}>取消</button>
|
<button class="absolute left-0" onclick={toggle_overlay}>取消</button>
|
||||||
<button class="absolute right-0">發送</button>
|
<button class="absolute right-0" onclickcapture={send}>發送</button>
|
||||||
</div>
|
</div>
|
||||||
<Marker text={value} height="h-[calc(100%-1.5rem)]" />
|
<Marker />
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
|
@ -1,15 +1,32 @@
|
|||||||
<script lang="ts">
|
<script module lang="ts">
|
||||||
let { text, height }: { text: string; height: string } = $props();
|
let content: string = $state<string>('');
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
|
||||||
import { Marked } from 'marked';
|
let dyn_files = $state<FileList>();
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import type { KeyboardEventHandler } from 'svelte/elements';
|
let files: Array<File> = [];
|
||||||
import { markedHighlight } from 'marked-highlight';
|
const get_content = () => {
|
||||||
import hljs from 'highlight.js';
|
let length = dyn_files == undefined ? 0 : dyn_files.length;
|
||||||
import '$lib/hljs/rose-pine.css';
|
for (let i = 0; i < length; i++) {
|
||||||
|
let item = dyn_files?.item(i);
|
||||||
|
if (item != null) {
|
||||||
|
files.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
const clear_content = () => {
|
||||||
|
content = '';
|
||||||
|
files.splice(0, files.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { files, get_content, clear_content };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import type { KeyboardEventHandler } from 'svelte/elements';
|
||||||
|
import MingcutePicFill from '~icons/mingcute/pic-fill';
|
||||||
|
|
||||||
let window_size = $state(0);
|
|
||||||
let value = $state('');
|
|
||||||
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
|
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
|
||||||
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
|
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
|
||||||
if (event.key == 'Tab') {
|
if (event.key == 'Tab') {
|
||||||
@ -20,73 +37,28 @@
|
|||||||
ct.selectionStart = ct.selectionEnd = start + 1;
|
ct.selectionStart = ct.selectionEnd = start + 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// marked
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth={window_size} />
|
<div class="flex h-full w-full flex-col justify-items-center gap-3 pt-[3%]">
|
||||||
<div class="{height} w-full">
|
<Textarea
|
||||||
<div class="h-[4%] w-full"></div>
|
class="mx-auto h-[80%] w-full resize-none bg-background text-base"
|
||||||
{#if window_size < 360}
|
onkeydowncapture={key_scrap}
|
||||||
<div>
|
placeholder="寫一些東西..."
|
||||||
<Textarea class="h-[96%] w-full resize-none" />
|
bind:value={content}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<div class="w-full">
|
||||||
|
<input
|
||||||
|
id="enclosed"
|
||||||
|
type="file"
|
||||||
|
accept=".gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp"
|
||||||
|
multiple
|
||||||
|
class="hidden"
|
||||||
|
bind:files={dyn_files}
|
||||||
|
/>
|
||||||
|
<label for="enclosed" class="mx-auto w-fit"><MingcutePicFill class="text-xl" /> </label>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="grid h-[96%] w-full grid-cols-2 gap-5">
|
|
||||||
<textarea
|
|
||||||
tabindex="0"
|
|
||||||
class="resize-none rounded-lg bg-background p-1 text-foreground focus:outline-none focus:ring focus:ring-primary-foreground"
|
|
||||||
bind:value
|
|
||||||
onkeydown={key_scrap}
|
|
||||||
></textarea>
|
|
||||||
<div class="inner_card">
|
|
||||||
{@html marked.parse(value).toString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.inner_card :global {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
a {
|
|
||||||
@apply text-cyan-500;
|
|
||||||
}
|
|
||||||
p:has(code) {
|
|
||||||
overflow-x: scroll;
|
|
||||||
padding: 10px 0px;
|
|
||||||
code {
|
|
||||||
/* @apply bg-amber-600; */
|
|
||||||
text-wrap: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
word-break: keep-all;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
/* @apply bg-amber-600; */
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,22 +1,57 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { content }: { content: string } = $props();
|
let { content }: { content: string } = $props();
|
||||||
import { marked } from 'marked';
|
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import { Marked } from 'marked';
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import { markedHighlight } from 'marked-highlight';
|
||||||
|
import hljs from 'highlight.js';
|
||||||
//TODO Need sanitizer
|
//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
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<Dialog.Root>
|
||||||
class="card_eff flex max-h-80 max-w-64 flex-col overflow-y-hidden bg-amber-400 px-5 font-bold text-slate-900"
|
<Dialog.Trigger
|
||||||
|
class="max-h-80 max-w-64 overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f]"
|
||||||
>
|
>
|
||||||
<div class="inner_card">
|
<div class="inner_card">
|
||||||
{@html marked(content)}
|
{#await parsed}
|
||||||
</div>
|
loading
|
||||||
|
{:then item}
|
||||||
|
{@html item}
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
{#await parsed}
|
||||||
|
loading
|
||||||
|
{:then item}
|
||||||
|
{@html item}
|
||||||
|
{/await}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card_eff {
|
|
||||||
filter: drop-shadow(5px 10px #f38f0f);
|
|
||||||
}
|
|
||||||
.inner_card :global {
|
.inner_card :global {
|
||||||
a {
|
a {
|
||||||
@apply text-cyan-500;
|
@apply text-cyan-500;
|
||||||
@ -25,5 +60,8 @@
|
|||||||
@apply bg-amber-600;
|
@apply bg-amber-600;
|
||||||
}
|
}
|
||||||
mask-image: linear-gradient(0deg, transparent, white 50%);
|
mask-image: linear-gradient(0deg, transparent, white 50%);
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px 5%;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<div
|
<div
|
||||||
class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' +
|
class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' +
|
||||||
(overlay ? 'overlay_blur grid place-items-center' : 'hidden')}
|
(overlay ? 'overlay_blur grid place-items-center' : 'hidden')}
|
||||||
|
role="dialog"
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
Binary file not shown.
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.
@ -3,5 +3,13 @@ import { defineConfig } from 'vite';
|
|||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit(), Icons({ compiler: 'svelte' })]
|
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