mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-25 02:09:05 +01:00
feat(tools): 共有リンクジェネレータ
This commit is contained in:
parent
1d4f218b98
commit
8f185deff8
1
.nuxtignore
Normal file
1
.nuxtignore
Normal file
@ -0,0 +1 @@
|
||||
pages/**/*-ignore.vue
|
@ -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/",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -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;
|
||||
}
|
30
components/g/BsCopyButton.vue
Normal file
30
components/g/BsCopyButton.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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: "権限"
|
||||
|
161
pages/tools/share-link-generator.vue
Normal file
161
pages/tools/share-link-generator.vue
Normal 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>
|
33
pages/tools/tools-template-ignore.vue
Normal file
33
pages/tools/tools-template-ignore.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user