mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-25 10:19:07 +01:00
parent
633c545c7c
commit
ea2080b624
@ -15,9 +15,9 @@ export default <NavSection[]>[
|
|||||||
to: "/tools/avatar-decoration-preview/",
|
to: "/tools/avatar-decoration-preview/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
i18n: "_aidConverter.title",
|
i18n: "_customEmojiPreview.title",
|
||||||
description: "_aidConverter.description",
|
description: "_customEmojiPreview.description",
|
||||||
to: "/tools/aid-converter/",
|
to: "/tools/custom-emoji-preview/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
i18n: "_shareLinkGenerator.title",
|
i18n: "_shareLinkGenerator.title",
|
||||||
@ -26,4 +26,14 @@ export default <NavSection[]>[
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "_tools._forAdmin.title",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
i18n: "_aidConverter.title",
|
||||||
|
description: "_aidConverter.description",
|
||||||
|
to: "/tools/aid-converter/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
];
|
];
|
@ -38,6 +38,10 @@ const rawUrl = computed(() => {
|
|||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
if (rawUrl.value == null) return null;
|
if (rawUrl.value == null) return null;
|
||||||
|
|
||||||
|
if (props.url) {
|
||||||
|
return rawUrl.value;
|
||||||
|
}
|
||||||
|
|
||||||
const proxied =
|
const proxied =
|
||||||
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
|
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
|
||||||
? parseURL(rawUrl.value).host ? rawUrl.value : 'https://misskey.io' + rawUrl.value
|
? parseURL(rawUrl.value).host ? rawUrl.value : 'https://misskey.io' + rawUrl.value
|
||||||
@ -51,7 +55,7 @@ const url = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const alt = computed(() => `:${customEmojiName.value}:`);
|
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||||
const errored = ref(props.host == null);
|
const errored = ref(props.host == null && !props.url);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -31,6 +31,8 @@ export default function(props: {
|
|||||||
rootScale?: number;
|
rootScale?: number;
|
||||||
/** 表示の基準にするMisskeyサーバーのドメイン */
|
/** 表示の基準にするMisskeyサーバーのドメイン */
|
||||||
baseHost?: string;
|
baseHost?: string;
|
||||||
|
/** 差し込むカスタム絵文字(:key:と画像URL) */
|
||||||
|
customEmojis?: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const isNote = props.isNote !== undefined ? props.isNote : true;
|
const isNote = props.isNote !== undefined ? props.isNote : true;
|
||||||
|
|
||||||
@ -281,7 +283,15 @@ export default function(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'emojiCode': {
|
case 'emojiCode': {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
if (props.customEmojis && props.customEmojis[token.props.name]) {
|
||||||
|
return [h(MkCustomEmoji, {
|
||||||
|
key: Math.random(),
|
||||||
|
name: token.props.name,
|
||||||
|
normal: props.plain,
|
||||||
|
useOriginalSize: scale >= 2.5,
|
||||||
|
url: props.customEmojis[token.props.name],
|
||||||
|
})]
|
||||||
|
}
|
||||||
return [h(MkCustomEmoji, {
|
return [h(MkCustomEmoji, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
name: token.props.name,
|
name: token.props.name,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-xl h-auto w-full px-4 py-3.5 md:px-8 md:py-7 flex max-w-sm border border-slate-300 dark:border-slate-950">
|
<div class="rounded-xl h-auto w-full px-4 py-3.5 md:px-8 md:py-7 flex max-w-md border border-slate-300 dark:border-slate-800">
|
||||||
<div class="mr-3.5">
|
<div class="mr-3.5">
|
||||||
<div class="w-[46px] h-[46px] md:w-[58px] md:h-[58px] relative rounded-full bg-slate-50 dark:bg-slate-900">
|
<div class="w-[46px] h-[46px] md:w-[58px] md:h-[58px] relative rounded-full bg-slate-50 dark:bg-slate-900">
|
||||||
<img class="w-full h-full object-cover rounded-full" :src="avatar" />
|
<img class="w-full h-full object-cover rounded-full" :src="avatar" />
|
||||||
@ -16,7 +16,14 @@
|
|||||||
<div class="font-bold">{{ $t('_avatarDecorationPreview._placeholder.username') }}</div>
|
<div class="font-bold">{{ $t('_avatarDecorationPreview._placeholder.username') }}</div>
|
||||||
<div class="opacity-70">@ai</div>
|
<div class="opacity-70">@ai</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ $t('_avatarDecorationPreview._placeholder.noteText') }}</div>
|
<div><slot>{{ $t('_avatarDecorationPreview._placeholder.noteText') }}</slot></div>
|
||||||
|
<div class="mt-1 flex flex-wrap gap-2">
|
||||||
|
<span v-for="reaction in reactions" class="inline-flex items-center space-x-1 px-1.5 py-1 rounded bg-gray-100 dark:bg-gray-800">
|
||||||
|
<MkCustomEmoji class="hover:!transform-none" v-if="reaction.name.startsWith(':')" :name="reaction.name" :url="reaction.url" :useOriginalSize="true" />
|
||||||
|
<div v-else class="text-xl leading-[1.35] select-none">{{ reaction.name }}</div>
|
||||||
|
<span class="text-sm">{{ reaction.count }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="flex space-x-3.5 md:space-x-7 opacity-60">
|
<div class="flex space-x-3.5 md:space-x-7 opacity-60">
|
||||||
<div class="p-2"><ReplyIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current reply" /></div>
|
<div class="p-2"><ReplyIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current reply" /></div>
|
||||||
<div class="p-2"><RenoteIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current" /></div>
|
<div class="p-2"><RenoteIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current" /></div>
|
||||||
@ -34,10 +41,17 @@ import RenoteIco from 'bi/repeat.svg';
|
|||||||
import ReactionIco from 'bi/plus-lg.svg';
|
import ReactionIco from 'bi/plus-lg.svg';
|
||||||
import MoreIco from 'bi/three-dots.svg';
|
import MoreIco from 'bi/three-dots.svg';
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(defineProps<{
|
||||||
avatar: string;
|
avatar?: string;
|
||||||
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
||||||
}>();
|
reactions?: {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
}>(), {
|
||||||
|
avatar: '/img/docs/fukidashi/doya_ai.webp',
|
||||||
|
});
|
||||||
|
|
||||||
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
||||||
const angle = decoration.angle ?? 0;
|
const angle = decoration.angle ?? 0;
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-xl h-auto w-72 border border-slate-300 dark:border-slate-950 bg-slate-200 dark:bg-slate-700">
|
<div class="rounded-xl h-auto w-72 border border-slate-300 dark:border-slate-800 bg-slate-200 dark:bg-slate-700">
|
||||||
<div class="mt-[84px] bg-white dark:bg-slate-950 rounded-b-xl divide-y divide-slate-200 dark:divide-slate-900">
|
<div class="mt-[84px] bg-white dark:bg-slate-950 rounded-b-xl divide-y divide-slate-200 dark:divide-slate-900">
|
||||||
<div class="flex items-center space-x-2 px-4 py-2">
|
<div class="flex items-center space-x-2 px-4 py-2">
|
||||||
<div class="w-[66px] h-[66px] relative -mt-8 rounded-full border-4 border-white dark:border-slate-950 bg-slate-50 dark:bg-slate-900">
|
<div class="w-[66px] h-[66px] relative -mt-8 rounded-full border-4 border-white dark:border-slate-950 bg-slate-50 dark:bg-slate-900">
|
||||||
@ -24,10 +24,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(defineProps<{
|
||||||
avatar: string;
|
avatar?: string;
|
||||||
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
||||||
}>();
|
}>(), {
|
||||||
|
avatar: '/img/docs/fukidashi/doya_ai.webp',
|
||||||
|
});
|
||||||
|
|
||||||
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
||||||
const angle = decoration.angle ?? 0;
|
const angle = decoration.angle ?? 0;
|
@ -7,6 +7,8 @@ share: "共有する"
|
|||||||
note: "ノート"
|
note: "ノート"
|
||||||
other: "その他"
|
other: "その他"
|
||||||
add: "追加"
|
add: "追加"
|
||||||
|
browse: "参照"
|
||||||
|
settings: "設定"
|
||||||
goToLegacyHub: "従来のMisskey Hub"
|
goToLegacyHub: "従来のMisskey Hub"
|
||||||
|
|
||||||
_error:
|
_error:
|
||||||
@ -209,6 +211,8 @@ _tools:
|
|||||||
menuToggle: "メニュー"
|
menuToggle: "メニュー"
|
||||||
_forUsers:
|
_forUsers:
|
||||||
title: "Misskeyユーザー向け"
|
title: "Misskeyユーザー向け"
|
||||||
|
_forAdmin:
|
||||||
|
title: "サーバー運営者向け"
|
||||||
|
|
||||||
_mfmPlayground:
|
_mfmPlayground:
|
||||||
title: "MFMお試しコーナー"
|
title: "MFMお試しコーナー"
|
||||||
@ -317,6 +321,18 @@ _avatarDecorationPreview:
|
|||||||
noteText: "チョコのかかったドーナツを食べました🍩😋"
|
noteText: "チョコのかかったドーナツを食べました🍩😋"
|
||||||
profileDescription: "Misskey常駐AIの藍です!よろしくお願いします♪"
|
profileDescription: "Misskey常駐AIの藍です!よろしくお願いします♪"
|
||||||
|
|
||||||
|
_customEmojiPreview:
|
||||||
|
title: "カスタム絵文字 プレビュー"
|
||||||
|
description: "カスタム絵文字の視認性をチェックできます。"
|
||||||
|
preview: "プレビュー"
|
||||||
|
emoji: "絵文字 #{number}"
|
||||||
|
placeholder: "「追加」から、カスタム絵文字を追加してプレビューできます。"
|
||||||
|
_options:
|
||||||
|
text: "ノート文面"
|
||||||
|
textDescription: "追加したカスタム絵文字は{emoji_id}で参照できます。"
|
||||||
|
_placeholder:
|
||||||
|
noteText: "カスタム絵文字はこんな感じで表示されます→ :emoji_preview_0:\n文章を書き換えて、使い勝手を試してみてくださいね✨"
|
||||||
|
|
||||||
_api:
|
_api:
|
||||||
_permissions:
|
_permissions:
|
||||||
title: "権限"
|
title: "権限"
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
{{ $t('_avatarDecorationPreview.preview') }}
|
{{ $t('_avatarDecorationPreview.preview') }}
|
||||||
</header>
|
</header>
|
||||||
<div class="flex gap-8 items-center justify-center flex-wrap">
|
<div class="flex gap-8 items-center justify-center flex-wrap">
|
||||||
<ToolsAvatarDecorationMkProf
|
<ToolsMocksMkProf
|
||||||
:avatar="avatar"
|
:avatar="avatar"
|
||||||
:decorations="realDecorations"
|
:decorations="realDecorations"
|
||||||
/>
|
/>
|
||||||
<ToolsAvatarDecorationMkNote
|
<ToolsMocksMkNote
|
||||||
:avatar="avatar"
|
:avatar="avatar"
|
||||||
:decorations="realDecorations"
|
:decorations="realDecorations"
|
||||||
/>
|
/>
|
||||||
|
162
pages/tools/custom-emoji-preview.vue
Normal file
162
pages/tools/custom-emoji-preview.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class='container mx-auto max-w-screen-xl px-6 py-6'>
|
||||||
|
<h1 class='text-2xl lg:text-3xl font-bold mb-4'>
|
||||||
|
{{ $t('_customEmojiPreview.title') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="p-6 space-y-4 rounded-lg bg-white dark:bg-slate-950">
|
||||||
|
<header class="-mt-6 -mx-6 px-6 py-3 font-bold text-lg border-b border-slate-300 dark:border-slate-800">
|
||||||
|
{{ $t('_customEmojiPreview.preview') }}
|
||||||
|
</header>
|
||||||
|
<div class="flex gap-8 items-center justify-center flex-wrap">
|
||||||
|
<ToolsMocksMkNote
|
||||||
|
class="!max-w-xl"
|
||||||
|
:reactions="[{
|
||||||
|
name: '👍',
|
||||||
|
count: 1,
|
||||||
|
}, ...noteReactions]"
|
||||||
|
>
|
||||||
|
<MkMfm :text="text" :customEmojis="customEmojisDefinition" />
|
||||||
|
</ToolsMocksMkNote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid md:grid-cols-5 gap-8">
|
||||||
|
<div class="p-6 space-y-4 rounded-lg bg-white dark:bg-slate-950 order-1 md:col-span-2 md:order-2">
|
||||||
|
<header class="-mt-6 -mx-6 px-6 py-3 font-bold text-lg border-b border-slate-300 dark:border-slate-800">
|
||||||
|
{{ $t('_customEmojiPreview._options.text') }}
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<label for="customEmojiPreviewText">{{ $t('_customEmojiPreview._options.text') }}</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="customEmojiPreviewText"
|
||||||
|
rows="10"
|
||||||
|
v-model="text"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 rounded-lg bg-white dark:bg-slate-950 order-2 md:col-span-3 md:order-1">
|
||||||
|
<header class="-mt-6 -mx-6 px-6 py-3 border-b border-slate-300 dark:border-slate-800 flex items-center">
|
||||||
|
<h2 class="font-bold text-lg">{{ $t('settings') }}</h2>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button class="btn btn-primary" @click="addEmoji()"><PlusIco class="mr-1" />{{ $t('add') }}</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div v-if="emojis.length === 0" class="p-8 text-center font-bold text-lg">
|
||||||
|
{{ $t('_customEmojiPreview.placeholder') }}
|
||||||
|
</div>
|
||||||
|
<div v-for="emoji, index in emojis" :key="emoji.id" class="mt-8 p-4 rounded-lg bg-white dark:bg-slate-950 border border-slate-300 dark:border-slate-800">
|
||||||
|
<h3 class="-mt-7 font-bold flex items-center w-fit px-3 bg-white dark:bg-slate-950 mb-4">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger mr-2" @click="deleteEmoji(index)"><XIco class="h-3.5 w-3.5 stroke-1 stroke-current" /></button>
|
||||||
|
<div>{{ $t('_customEmojiPreview.emoji', { number: emoji.id }) }}</div>
|
||||||
|
</h3>
|
||||||
|
<div class="lg:flex items-center space-y-4 lg:space-y-0 lg:space-x-4">
|
||||||
|
<div class="mx-auto lg:mx-0 w-fit space-y-2 flex-shrink-0">
|
||||||
|
<div
|
||||||
|
class="h-16 p-2 rounded-lg max-w-[200px] transition-colors"
|
||||||
|
:class="emoji.invertColorScheme ? 'bg-slate-800 dark:bg-slate-200' : 'bg-slate-200 dark:bg-slate-800'"
|
||||||
|
>
|
||||||
|
<img class="h-full w-full object-contain" :src="getEmojiUrl(emoji)" />
|
||||||
|
</div>
|
||||||
|
<div class="w-min mx-auto form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" :id="`invertColorSchemeSwitch_${emoji.id}`" v-model="emojis[index].invertColorScheme">
|
||||||
|
<label v-if="colorMode.value === 'dark'" class="form-check-label" :for="`invertColorSchemeSwitch_${emoji.id}`"><SunIco /></label>
|
||||||
|
<label v-else class="form-check-label" :for="`invertColorSchemeSwitch_${emoji.id}`"><MoonIco /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<I18nT scope="global" keypath="_customEmojiPreview._options.textDescription" tag="div">
|
||||||
|
<template #emoji_id>
|
||||||
|
<code class="inline-block text-sm bg-slate-200 dark:bg-slate-800 mx-0.5 p-0.5 rounded">{{ noteReactions[index].name }}</code>
|
||||||
|
</template>
|
||||||
|
</I18nT>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import PlusIco from 'bi/plus-lg.svg';
|
||||||
|
import XIco from 'bi/x-lg.svg';
|
||||||
|
import SunIco from 'bi/sun.svg';
|
||||||
|
import MoonIco from 'bi/moon.svg';
|
||||||
|
import { on } from 'events';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'tools',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const colorMode = useColorMode();
|
||||||
|
|
||||||
|
const text = ref(t('_customEmojiPreview._placeholder.noteText'));
|
||||||
|
|
||||||
|
const emojis = ref<{
|
||||||
|
id: number;
|
||||||
|
file: string | Blob;
|
||||||
|
invertColorScheme: boolean;
|
||||||
|
}[]>([{
|
||||||
|
id: 0,
|
||||||
|
file: '/img/emojis/rocket_3d.png',
|
||||||
|
invertColorScheme: false,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const noteReactions = computed(() => emojis.value?.map((emoji) => ({
|
||||||
|
name: `:emoji_preview_${emoji.id}:`,
|
||||||
|
code: `emoji_preview_${emoji.id}`,
|
||||||
|
url: (typeof emoji.file !== 'string') ? URL.createObjectURL(emoji.file) : emoji.file,
|
||||||
|
count: 1,
|
||||||
|
})) ?? []);
|
||||||
|
|
||||||
|
const customEmojisDefinition = computed(() => Object.fromEntries(noteReactions.value?.map((emoji) => [ emoji.code, emoji.url ]) ?? []));
|
||||||
|
|
||||||
|
function deleteEmoji(index: number) {
|
||||||
|
emojis.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmojiUrl(emoji: { id: number; file: string | Blob; }) {
|
||||||
|
return (typeof emoji.file !== 'string') ? URL.createObjectURL(emoji.file) : emoji.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEmoji() {
|
||||||
|
if (!process.client) return;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.addEventListener('change', (event) => {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
emojis.value.push({
|
||||||
|
id: emojis.value.length,
|
||||||
|
file,
|
||||||
|
invertColorScheme: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
input.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
for (const emoji of noteReactions.value) {
|
||||||
|
if (emoji.url.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(emoji.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emojis.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
route.meta.title = t('_customEmojiPreview.title');
|
||||||
|
route.meta.description = t('_customEmojiPreview.description');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user