feat(tools): 共有リンクジェネレータ

This commit is contained in:
kakkokari-gtyih 2023-12-04 22:47:06 +09:00
parent 1d4f218b98
commit 8f185deff8
8 changed files with 294 additions and 3 deletions

1
.nuxtignore Normal file
View File

@ -0,0 +1 @@
pages/**/*-ignore.vue

View File

@ -13,7 +13,12 @@ export default <NavSection[]>[
i18n: "_aidConverter.title",
description: "_aidConverter.description",
to: "/tools/aid-converter/",
}
]
}
},
{
i18n: "_shareLinkGenerator.title",
description: "_shareLinkGenerator.description",
to: "/tools/share-link-generator/",
},
],
},
];

View File

@ -73,3 +73,39 @@ export const findDeepObject = (obj: NavItem, condition: (v: NavItem) => boolean)
return null;
};
/**
* Clipboardに値をコピー(TODO: 文字列以外も対応)
*/
export function copyText(val: string) {
if (!process.client) return;
// 空div 生成
const tmp = document.createElement('div');
// 選択用のタグ生成
const pre = document.createElement('pre');
// 親要素のCSSで user-select: none だとコピーできないので書き換える
pre.style.webkitUserSelect = 'auto';
pre.style.userSelect = 'auto';
tmp.appendChild(pre).textContent = val;
// 要素を画面外へ
const s = tmp.style;
s.position = 'fixed';
s.right = '200%';
// body に追加
document.body.appendChild(tmp);
// 要素を選択
document.getSelection()?.selectAllChildren(tmp);
// クリップボードにコピー
const result = document.execCommand('copy');
// 要素削除
document.body.removeChild(tmp);
return result;
}

View File

@ -0,0 +1,30 @@
<template>
<button class="btn btn-outline-primary hover:!text-white focus-visible:!text-white" @click="copy">
<CopyIco class="w-4 h-4" v-if="!copied" />
<CheckIco class="w-4 h-4" v-else />
</button>
</template>
<script setup lang="ts">
import { copyText } from '@/assets/js/misc';
import CopyIco from 'bi/clipboard.svg';
import CheckIco from 'bi/check-lg.svg';
const props = defineProps<{
text: string;
}>();
const copied = ref(false);
function copy() {
copyText(props.text);
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 3000);
}
</script>
<style scoped>
</style>

View File

@ -53,6 +53,7 @@ const route = useRoute();
<div :class="$style.slimPageRoot">
<slot />
</div>
<GFooter class="mt-12 rounded-tl-3xl lg:rounded-tl-none bg-white dark:bg-slate-950" />
</main>
</div>
</div>

View File

@ -2,6 +2,7 @@ noScript: "現在Javascriptが無効になっています。サイトの表示
learnMore: "詳しく知る"
loading: "読み込み中…"
clickToExpand: "(クリックして展開)"
copy: "コピー"
_seo:
siteName: "Misskey Hub"
@ -238,6 +239,29 @@ _share:
domain: "サーバーのドメイン"
compatibleWith: "Misskeyと、一部のMisskeyフォークに対応しています。"
_noteVisibility:
public: "パブリック"
home: "ホーム"
followers: "フォロワー"
specified: "ダイレクト"
localOnly: "連合なし"
_shareLinkGenerator:
title: "共有ボタンジェネレーター"
description: "Misskey Hubの共有ボタン中継サービスを利用して、Misskey用の共有ボタンを作成できます。"
body: "本文"
bodyWarning: "どのサーバーでも共有できるようにするため、カスタム絵文字は使用できません。"
url: "URL"
urlCaption: "任意。本文の後ろに挿入されます。"
settings: "詳細設定"
visibility: "公開範囲"
recipents: "ダイレクトを受け取る人のacct改行区切り"
resultLink: "リンク生成結果"
resultButton: "共有ボタンのサンプル"
testLink: "共有リンクを試す"
typeSomethingToGetLink: "本文を入力するとリンクが生成されます。"
typeSomethingToGetButton: "本文を入力するとボタンが生成されます。"
_api:
_permissions:
title: "権限"

View File

@ -0,0 +1,161 @@
<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('_shareLinkGenerator.title') }}
</h1>
<div class='rounded-lg grid md:grid-cols-2 gap-8'>
<div class="space-y-4">
<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">
{{ $t('_shareLinkGenerator.body') }}
</header>
<div>
<div class="flex">
<label for="shareLinkGeneratorBody">{{ $t('_shareLinkGenerator.body') }}</label>
<div
class="ml-auto"
:class="[(mfmText?.length ?? 0) >= 5000 && 'font-bold text-red-600']"
>
{{ $t('_mfmPlayground.character', [ (mfmText?.length ?? 0) ]) }}
</div>
</div>
<textarea
:rows="(mfmText || '').split('\n').length >= 8 ? (mfmText || '').split('\n').length + 5 : 10"
class="form-control"
id="shareLinkGeneratorBody"
v-model="mfmText"
></textarea>
<div class="form-text">{{ $t('_shareLinkGenerator.bodyWarning') }}</div>
</div>
<div>
<label for="shareLinkGeneratorUrl">{{ $t('_shareLinkGenerator.url') }}</label>
<input class="form-control" id="shareLinkGeneratorUrl" v-model="mfmUrl" />
<div class="form-text">{{ $t('_shareLinkGenerator.urlCaption') }}</div>
</div>
<div>
{{ $t('_mfmPlayground.preview') }}
<div :class="$style.mfmRoot" class="mb-2 bg-white dark:bg-[#212529] border-gray-200 dark:border-gray-600">
<MkMfm :text="mfmPreviewString" />
</div>
<div class="form-text">{{ $t('_mfmPlayground.disclaimer') }}</div>
</div>
</div>
<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">
{{ $t('_shareLinkGenerator.settings') }}
</header>
<div>
<label for="shareLinkGeneratorVisibility">{{ $t('_shareLinkGenerator.visibility') }}</label>
<select class="form-select" id="shareLinkGeneratorVisibility" v-model="visibility">
<option v-for="visibility in noteVisibilities" :key="visibility" :value="visibility">{{ $t(`_noteVisibility.${visibility}`) }}</option>
</select>
</div>
<div v-if="visibility === 'specified'">
<label for="shareLinkGeneratorRecipents">{{ $t('_shareLinkGenerator.recipents') }}</label>
<textarea
class="form-control"
id="shareLinkGeneratorRecipents"
placeholder="@someone@misskey.example.com"
v-model="recipents"
:rows="(recipents || '').split('\n').length >= 5 ? (recipents || '').split('\n').length + 2 : 7"
></textarea>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="localOnly" id="shareLinkGeneratorLocalOnly">
<label class="form-check-label" for="shareLinkGeneratorLocalOnly">
{{ $t('_noteVisibility.localOnly') }}
</label>
</div>
</div>
</div>
<div class="space-y-4">
<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">
{{ $t('_shareLinkGenerator.resultLink') }}
</header>
<div v-if="(mfmText?.length ?? 0) === 0" class="p-8 text-center font-bold text-lg">
{{ $t('_shareLinkGenerator.typeSomethingToGetLink') }}
</div>
<template v-else>
<div class="input-group">
<input readonly class="form-control" :value="generatedUrl" />
<GBsCopyButton :text="generatedUrl" />
</div>
<div class="form-text">
<GNuxtLink :to="generatedUrl" target="_blank" class="hover:underline underline-offset-1">{{ $t('_shareLinkGenerator.testLink') }}<ExtIco class="ml-1" /></GNuxtLink>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import { noteVisibilities } from 'misskey-js';
import { joinURL, withQuery } from 'ufo';
import ExtIco from 'bi/box-arrow-up-right.svg';
import type { entities as MisskeyEntities } from 'misskey-js';
import type { QueryObject } from 'ufo';
definePageMeta({
layout: 'tools',
});
const mfmText = ref<string>('');
const mfmUrl = ref<string>('');
const visibility = ref<MisskeyEntities.Note['visibility']>('public');
const localOnly = ref<boolean>(false);
const recipents = ref<string>('');
const runtimeConfig = useRuntimeConfig();
const mfmPreviewString = computed(() => {
if (mfmText.value != '') {
if (mfmUrl.value != '') {
return mfmText.value + '\n' + mfmUrl.value;
} else {
return mfmText.value;
}
} else if (mfmUrl.value != '') {
return mfmUrl.value;
} else {
return '';
}
})
const generatedUrl = computed(() => {
const query: QueryObject = {
text: mfmText.value,
url: (mfmUrl.value != '') ? mfmUrl.value : undefined,
visibility: visibility.value,
localOnly: localOnly.value ? '1' : '0',
visibleAccts: (visibility.value === 'specified') ? recipents.value.split('\n').join(',') : undefined,
};
return withQuery(joinURL(runtimeConfig.public.baseUrl, '/share/'), query);
});
const generatedHtml = computed(() => '');
const { t } = useI18n();
const route = useRoute();
route.meta.title = t('_shareLinkGenerator.title');
route.meta.description = t('_shareLinkGenerator.description');
</script>
<style module>
.mfmRoot {
@apply rounded-lg p-4 border break-words overflow-hidden;
font-family: Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;
line-height: 1.35;
}
.shareLink {
}
.shareLinkIcon {
}
</style>

View File

@ -0,0 +1,33 @@
<!--
ツール集のテンプレート
翻訳キーの `aaaaaaaa` となっている場所を置き換えればあとは普通のVueとほぼ同じ
コンポーネントは命名規則に従っている限り自動インポート
ref/reactiveなどのvue関連も自動インポート
-->
<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('_aaaaaaaa.title') }}
</h1>
<!-- ここに書く -->
</div>
</template>
<script setup lang='ts'>
definePageMeta({
layout: 'tools',
});
const { t } = useI18n();
const route = useRoute();
route.meta.title = t('_aaaaaaaa.title');
route.meta.description = t('_aaaaaaaa.description');
</script>
<style module>
</style>