feat: organized config

This commit is contained in:
Shusui MOYATANI
2023-05-20 03:58:36 +09:00
parent 067b0a2973
commit 9c902b1b40
6 changed files with 190 additions and 83 deletions

View File

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

View File

@@ -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>
); );
}; };

View File

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

View File

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

View File

@@ -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
View 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());
};