mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
feat: organized config
This commit is contained in:
@@ -343,13 +343,11 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
{/*
|
||||
<EmojiPicker customEmojis={true} onEmojiSelect={(emoji) => appendText(emoji)}>
|
||||
<span class="inline-block h-8 w-8 rounded bg-primary p-2 font-bold text-white">
|
||||
<FaceSmile />
|
||||
</span>
|
||||
</EmojiPicker>
|
||||
*/}
|
||||
<button
|
||||
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
||||
classList={{
|
||||
|
||||
@@ -10,38 +10,7 @@ type ImageDisplayProps = {
|
||||
|
||||
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||
let imageRef: HTMLImageElement | undefined;
|
||||
let canvasRef: HTMLCanvasElement | undefined;
|
||||
|
||||
const [hidden, setHidden] = createSignal(props.initialHidden);
|
||||
const [playing, setPlaying] = createSignal(true);
|
||||
|
||||
// const isGIF = () => props.url.match(/\.gif/i);
|
||||
|
||||
const play = () => {
|
||||
setPlaying(true);
|
||||
};
|
||||
|
||||
/*
|
||||
const stop = () => {
|
||||
if (canvasRef == null || imageRef == null) return;
|
||||
canvasRef.width = imageRef.width;
|
||||
canvasRef.height = imageRef.height;
|
||||
canvasRef
|
||||
.getContext('2d')
|
||||
?.drawImage(
|
||||
imageRef,
|
||||
0,
|
||||
0,
|
||||
imageRef.naturalWidth,
|
||||
imageRef.naturalHeight,
|
||||
0,
|
||||
0,
|
||||
imageRef.width,
|
||||
imageRef.height,
|
||||
);
|
||||
setPlaying(false);
|
||||
};
|
||||
*/
|
||||
|
||||
return (
|
||||
<Show
|
||||
@@ -59,43 +28,10 @@ const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||
<img
|
||||
ref={imageRef}
|
||||
class="max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
||||
classList={{
|
||||
'inline-block': playing(),
|
||||
hidden: !playing(),
|
||||
}}
|
||||
src={playing() ? fixUrl(props.url) : undefined}
|
||||
src={fixUrl(props.url)}
|
||||
alt={props.url}
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
class="inline-block max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
||||
classList={{
|
||||
'w-0': playing(),
|
||||
'h-0': playing(),
|
||||
'w-auto': !playing(),
|
||||
'h-auto': !playing(),
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
play();
|
||||
}}
|
||||
/>
|
||||
</SafeLink>
|
||||
{/*
|
||||
<Show when={isGIF()}>
|
||||
<button
|
||||
class=""
|
||||
onClick={() => {
|
||||
if (playing()) stop();
|
||||
else play();
|
||||
}}
|
||||
>
|
||||
<Show when={!playing()} fallback="⏸">
|
||||
▶
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
*/}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (item.type === 'CustomEmoji') {
|
||||
const emojiUrl = event().getEmojiUrl(item.shortcode);
|
||||
if (emojiUrl == null) return <span>{item.content}</span>;
|
||||
// const { imageRef, canvas } = useImageAnimation({ initialPlaying: false });
|
||||
return (
|
||||
<img
|
||||
class="inline-block h-8 max-w-[128px] align-middle"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, createResource, For, Show } from 'solid-js';
|
||||
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import resolveAsset from '@/utils/resolveAsset';
|
||||
|
||||
type AboutProps = {
|
||||
@@ -46,6 +47,31 @@ const About: Component<AboutProps> = (props) => {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<h2 class="my-4 text-xl font-bold">バグ報告について</h2>
|
||||
|
||||
<p class="my-4">
|
||||
おかしな動作を見つけたら
|
||||
<a
|
||||
class="text-blue-500 underline"
|
||||
href="https://github.com/syusui-s/rabbit/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHubのIssues
|
||||
</a>
|
||||
までご報告ください。
|
||||
</p>
|
||||
|
||||
<h2 class="my-4 text-xl font-bold">ソースコード</h2>
|
||||
|
||||
<p class="my-4">
|
||||
ソースコードは
|
||||
<SafeLink class="text-blue-400 underline" href="https://github.com/syusui-s/rabbit">
|
||||
GitHub
|
||||
</SafeLink>
|
||||
で入手できます。
|
||||
</p>
|
||||
|
||||
<h2 class="my-4 text-xl font-bold">利用規約</h2>
|
||||
|
||||
<p class="my-4">Copyright (C) 2023 Shusui Moyatani</p>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { createSignal, For, type JSX } from 'solid-js';
|
||||
import { createSignal, Show, For, type JSX } from 'solid-js';
|
||||
|
||||
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
||||
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
||||
import FaceSmile from 'heroicons/24/outline/face-smile.svg';
|
||||
import PaintBrush from 'heroicons/24/outline/paint-brush.svg';
|
||||
import ServerStack from 'heroicons/24/outline/server-stack.svg';
|
||||
import User from 'heroicons/24/outline/user.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
@@ -231,7 +237,7 @@ const EmojiConfig = () => {
|
||||
<For each={Object.values(config().customEmojis)}>
|
||||
{({ shortcode, url }) => (
|
||||
<li class="flex items-center gap-2">
|
||||
<img class="h-7 max-w-[128px]" src={url} alt={shortcode} />
|
||||
<img class="min-w-7 h-7 max-w-[128px]" src={url} alt={shortcode} />
|
||||
<div class="flex-1 truncate">{shortcode}</div>
|
||||
<button class="h-3 w-3 shrink-0" onClick={() => removeEmoji(shortcode)}>
|
||||
<XMark />
|
||||
@@ -240,9 +246,9 @@ const EmojiConfig = () => {
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<form class="flex gap-2" onSubmit={handleClickSaveEmoji}>
|
||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||
<label class="flex flex-1 items-center gap-1">
|
||||
<div>名前</div>
|
||||
<div class="w-9">名前</div>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
type="text"
|
||||
@@ -254,7 +260,7 @@ const EmojiConfig = () => {
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-1 items-center gap-1">
|
||||
<div>URL</div>
|
||||
<div class="w-9">URL</div>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
type="text"
|
||||
@@ -266,8 +272,8 @@ const EmojiConfig = () => {
|
||||
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||
保存
|
||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||
追加
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -395,18 +401,84 @@ const OtherConfig = () => {
|
||||
};
|
||||
|
||||
const ConfigUI = (props: ConfigProps) => {
|
||||
// <div class="max-h-[90vh] w-[640px] max-w-[100vw] overflow-y-scroll rounded bg-white p-4 shadow">
|
||||
const [menuIndex, setMenuIndex] = createSignal<number | null>(null);
|
||||
|
||||
const menu = [
|
||||
{
|
||||
name: () => 'プロフィール',
|
||||
icon: () => <User />,
|
||||
render: () => <ProfileSection />,
|
||||
},
|
||||
{
|
||||
name: () => 'リレー',
|
||||
icon: () => <ServerStack />,
|
||||
render: () => <RelayConfig />,
|
||||
},
|
||||
{
|
||||
name: () => '表示',
|
||||
icon: () => <PaintBrush />,
|
||||
render: () => (
|
||||
<>
|
||||
<DateFormatConfig />
|
||||
<ReactionConfig />
|
||||
<OtherConfig />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: () => 'カスタム絵文字',
|
||||
icon: () => <FaceSmile />,
|
||||
render: () => <EmojiConfig />,
|
||||
},
|
||||
{
|
||||
name: () => 'ミュート',
|
||||
icon: () => <EyeSlash />,
|
||||
render: () => <MuteConfig />,
|
||||
},
|
||||
];
|
||||
|
||||
const getMenuItem = () => {
|
||||
const index = menuIndex();
|
||||
if (index == null) return null;
|
||||
return menu[index];
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="p-4">
|
||||
<h2 class="flex-1 text-center text-lg font-bold">設定</h2>
|
||||
<ProfileSection />
|
||||
<RelayConfig />
|
||||
<DateFormatConfig />
|
||||
<ReactionConfig />
|
||||
<EmojiConfig />
|
||||
<OtherConfig />
|
||||
<MuteConfig />
|
||||
<Show
|
||||
when={getMenuItem()}
|
||||
fallback={
|
||||
<>
|
||||
<h2 class="flex-1 text-center text-lg font-bold">設定</h2>
|
||||
<ul class="flex flex-col">
|
||||
<For each={menu}>
|
||||
{(menuItem, i) => (
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="flex w-full gap-2 py-3 hover:text-rose-400"
|
||||
onClick={() => setMenuIndex(i)}
|
||||
>
|
||||
<span class="inline-block h-6 w-6">{menuItem.icon()}</span>
|
||||
{menuItem.name()}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
keyed
|
||||
>
|
||||
{(menuItem) => (
|
||||
<div class="flex flex-col">
|
||||
<button class="inline-block h-6 w-6" onClick={() => setMenuIndex(null)}>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<div class="w-full flex-1 pt-4">{menuItem.render()}</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</BasicModal>
|
||||
);
|
||||
|
||||
74
src/utils/emojipack.ts
Normal file
74
src/utils/emojipack.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* This file is licensed under MIT license, not AGPL.
|
||||
*
|
||||
* Copyright (c) 2023 Syusui Moyatani
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const pubkeySchema = z
|
||||
.string()
|
||||
.length(64)
|
||||
.regex(/^[0-9a-f]{64}$/);
|
||||
|
||||
export const shortcodeSchema = z.string().regex(/^\w+$/);
|
||||
|
||||
export const emojiSchema = z.object({
|
||||
shortcode: shortcodeSchema,
|
||||
url: z.string().url(),
|
||||
keywords: z.optional(z.array(shortcodeSchema)),
|
||||
});
|
||||
|
||||
export const emojiPackSchemaV1 = z
|
||||
.object({
|
||||
manifest: z.literal('emojipack_v1'),
|
||||
name: z.string(),
|
||||
emojis: z.array(emojiSchema),
|
||||
description: z.optional(z.string()),
|
||||
author: z.optional(pubkeySchema),
|
||||
publisher: z.optional(pubkeySchema),
|
||||
})
|
||||
.refine(
|
||||
(emojiPack) => {
|
||||
// check uniqueness of shortcodes
|
||||
const uniqEmojis = uniqBy(emojiPack.emojis, (emoji) => emoji.shortcode).length;
|
||||
return uniqEmojis === emojiPack.emojis.length;
|
||||
},
|
||||
{ message: 'shortcodes should be unique' },
|
||||
);
|
||||
|
||||
export const simpleEmojiPackSchema = z.record(shortcodeSchema, z.string().url());
|
||||
|
||||
export const allEmojiPackSchema = emojiPackSchemaV1.or(simpleEmojiPackSchema);
|
||||
|
||||
export type Emoji = z.infer<typeof emojiSchema>;
|
||||
|
||||
export type EmojiPackV1 = z.infer<typeof emojiPackSchemaV1>;
|
||||
|
||||
export type SimpleEmojiPack = z.infer<typeof simpleEmojiPackSchema>;
|
||||
|
||||
export type AllEmojiPack = z.infer<typeof allEmojiPackSchema>;
|
||||
|
||||
export const getEmojiPack = async (urlString: string): Promise<AllEmojiPack> => {
|
||||
const url = new URL(urlString);
|
||||
const res = await fetch(url);
|
||||
return allEmojiPackSchema.parseAsync(await res.json());
|
||||
};
|
||||
Reference in New Issue
Block a user