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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
{/*
|
|
||||||
<EmojiPicker customEmojis={true} onEmojiSelect={(emoji) => appendText(emoji)}>
|
<EmojiPicker customEmojis={true} onEmojiSelect={(emoji) => appendText(emoji)}>
|
||||||
<span class="inline-block h-8 w-8 rounded bg-primary p-2 font-bold text-white">
|
<span class="inline-block h-8 w-8 rounded bg-primary p-2 font-bold text-white">
|
||||||
<FaceSmile />
|
<FaceSmile />
|
||||||
</span>
|
</span>
|
||||||
</EmojiPicker>
|
</EmojiPicker>
|
||||||
*/}
|
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
@@ -10,38 +10,7 @@ type ImageDisplayProps = {
|
|||||||
|
|
||||||
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||||
let imageRef: HTMLImageElement | undefined;
|
let imageRef: HTMLImageElement | undefined;
|
||||||
let canvasRef: HTMLCanvasElement | undefined;
|
|
||||||
|
|
||||||
const [hidden, setHidden] = createSignal(props.initialHidden);
|
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 (
|
return (
|
||||||
<Show
|
<Show
|
||||||
@@ -59,43 +28,10 @@ const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
|||||||
<img
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
class="max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
class="max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
|
||||||
classList={{
|
src={fixUrl(props.url)}
|
||||||
'inline-block': playing(),
|
|
||||||
hidden: !playing(),
|
|
||||||
}}
|
|
||||||
src={playing() ? fixUrl(props.url) : undefined}
|
|
||||||
alt={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>
|
</SafeLink>
|
||||||
{/*
|
|
||||||
<Show when={isGIF()}>
|
|
||||||
<button
|
|
||||||
class=""
|
|
||||||
onClick={() => {
|
|
||||||
if (playing()) stop();
|
|
||||||
else play();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={!playing()} fallback="⏸">
|
|
||||||
▶
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
*/}
|
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
|||||||
if (item.type === 'CustomEmoji') {
|
if (item.type === 'CustomEmoji') {
|
||||||
const emojiUrl = event().getEmojiUrl(item.shortcode);
|
const emojiUrl = event().getEmojiUrl(item.shortcode);
|
||||||
if (emojiUrl == null) return <span>{item.content}</span>;
|
if (emojiUrl == null) return <span>{item.content}</span>;
|
||||||
|
// const { imageRef, canvas } = useImageAnimation({ initialPlaying: false });
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
class="inline-block h-8 max-w-[128px] align-middle"
|
class="inline-block h-8 max-w-[128px] align-middle"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, createResource, For, Show } from 'solid-js';
|
import { Component, createResource, For, Show } from 'solid-js';
|
||||||
|
|
||||||
import BasicModal from '@/components/modal/BasicModal';
|
import BasicModal from '@/components/modal/BasicModal';
|
||||||
|
import SafeLink from '@/components/utils/SafeLink';
|
||||||
import resolveAsset from '@/utils/resolveAsset';
|
import resolveAsset from '@/utils/resolveAsset';
|
||||||
|
|
||||||
type AboutProps = {
|
type AboutProps = {
|
||||||
@@ -46,6 +47,31 @@ const About: Component<AboutProps> = (props) => {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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>
|
<h2 class="my-4 text-xl font-bold">利用規約</h2>
|
||||||
|
|
||||||
<p class="my-4">Copyright (C) 2023 Shusui Moyatani</p>
|
<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 XMark from 'heroicons/24/outline/x-mark.svg';
|
||||||
|
|
||||||
import BasicModal from '@/components/modal/BasicModal';
|
import BasicModal from '@/components/modal/BasicModal';
|
||||||
@@ -231,7 +237,7 @@ const EmojiConfig = () => {
|
|||||||
<For each={Object.values(config().customEmojis)}>
|
<For each={Object.values(config().customEmojis)}>
|
||||||
{({ shortcode, url }) => (
|
{({ shortcode, url }) => (
|
||||||
<li class="flex items-center gap-2">
|
<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>
|
<div class="flex-1 truncate">{shortcode}</div>
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeEmoji(shortcode)}>
|
<button class="h-3 w-3 shrink-0" onClick={() => removeEmoji(shortcode)}>
|
||||||
<XMark />
|
<XMark />
|
||||||
@@ -240,9 +246,9 @@ const EmojiConfig = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</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">
|
<label class="flex flex-1 items-center gap-1">
|
||||||
<div>名前</div>
|
<div class="w-9">名前</div>
|
||||||
<input
|
<input
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -254,7 +260,7 @@ const EmojiConfig = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-1 items-center gap-1">
|
<label class="flex flex-1 items-center gap-1">
|
||||||
<div>URL</div>
|
<div class="w-9">URL</div>
|
||||||
<input
|
<input
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -266,8 +272,8 @@ const EmojiConfig = () => {
|
|||||||
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,18 +401,84 @@ const OtherConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConfigUI = (props: ConfigProps) => {
|
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 (
|
return (
|
||||||
<BasicModal onClose={props.onClose}>
|
<BasicModal onClose={props.onClose}>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
<Show
|
||||||
|
when={getMenuItem()}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
<h2 class="flex-1 text-center text-lg font-bold">設定</h2>
|
<h2 class="flex-1 text-center text-lg font-bold">設定</h2>
|
||||||
<ProfileSection />
|
<ul class="flex flex-col">
|
||||||
<RelayConfig />
|
<For each={menu}>
|
||||||
<DateFormatConfig />
|
{(menuItem, i) => (
|
||||||
<ReactionConfig />
|
<li class="w-full">
|
||||||
<EmojiConfig />
|
<button
|
||||||
<OtherConfig />
|
class="flex w-full gap-2 py-3 hover:text-rose-400"
|
||||||
<MuteConfig />
|
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>
|
</div>
|
||||||
</BasicModal>
|
</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