mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-21 16:33:48 +01:00
(add) Misskey内部へのディープリンク
This commit is contained in:
parent
480af5759f
commit
ef45939008
@ -18,6 +18,12 @@ Release date: **TBD(未定)**
|
||||
|
||||
内部リンク・外部リンクに関する処理を強化した[`<GNuxtLink>`](./components/g/NuxtLink.vue)を使用していますので、**リンクを追加する際は`<NuxtLink>`ではなく`<GNuxtLink>`を使用してください。**
|
||||
|
||||
### Misskey Webへのリンクについて
|
||||
|
||||
GNuxtLinkおよび各種Docsで、アドレスに `x-mi-web://` から始め、続けてMisskeyの相対パスを入力すると、Misskey Webへのリンクに置き換えられます。
|
||||
|
||||
例: `/play` → `x-mi-web://play`
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import MiIco from '@/assets/svg/misskey_mi_bi.svg';
|
||||
import ExtIco from 'bi/box-arrow-up-right.svg';
|
||||
import { $URL, isRelative, joinURL } from 'ufo';
|
||||
import { isLocalPath, sanitizeInternalPath } from '@/assets/js/misc';
|
||||
@ -24,6 +25,7 @@ const realHref = ref(props.href);
|
||||
const realTarget = ref(props.target);
|
||||
|
||||
const url = new $URL(realHref.value);
|
||||
|
||||
if (isLocalPath(realHref.value)) {
|
||||
// 内部リンクの場合
|
||||
const route = resolve(realHref.value);
|
||||
@ -43,6 +45,8 @@ if (isLocalPath(realHref.value)) {
|
||||
|
||||
<template>
|
||||
<GNuxtLink :to="realHref" :target="realTarget">
|
||||
<slot></slot><ExtIco v-if="realTarget === '_blank'" class="text-xs mx-1" />
|
||||
<slot></slot>
|
||||
<MiIco v-if="realHref.startsWith('x-mi-web://')" class="text-xs mx-1" />
|
||||
<ExtIco v-else-if="realTarget === '_blank'" class="text-xs mx-1" />
|
||||
</GNuxtLink>
|
||||
</template>
|
222
components/g/MisskeyGateway.vue
Normal file
222
components/g/MisskeyGateway.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<div class="sm:py-20 sm:max-w-xl mx-auto">
|
||||
<div class="p-6 bg-white dark:bg-gray-800 sm:rounded-lg min-h-screen sm:min-h-[230px]">
|
||||
<div v-if="loading" class="mx-auto py-12">
|
||||
<MkLoading />
|
||||
</div>
|
||||
<div v-else class="space-y-6">
|
||||
<div class="w-12 h-12 mx-auto bg-accent-600/20 text-accent-600 rounded-full p-3">
|
||||
<component :is="branding?.icon ?? ShareIco" class="w-6 h-6" />
|
||||
</div>
|
||||
<h1 class="font-bold text-center text-lg md:text-xl">{{ branding?.heading ?? $t('_share.chooseServer') }}</h1>
|
||||
<ul class="rounded-lg border divide-y border-gray-300 dark:border-gray-600 divide-gray-300 dark:divide-gray-600">
|
||||
<li v-for="instance in displayInstances" :key="instance.url" class="group">
|
||||
<GNuxtLink
|
||||
:to="joinURL(`https://${instance.url}/`, path)"
|
||||
class="group-first:rounded-t-lg group-last:rounded-b-lg p-4 w-full flex items-center hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-600"
|
||||
>
|
||||
<div
|
||||
class="h-9 w-9 flex-shrink-0 overflow-hidden rounded bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 mr-3"
|
||||
:class="instance.isUserDefined && 'transition-[border-color] hover:border-red-600 dark:hover:border-red-600'"
|
||||
>
|
||||
<template v-if="instance.isUserDefined">
|
||||
<button class="relative w-full h-full group/delete" @click.stop.prevent="deleteInstance(instance.url)">
|
||||
<img v-if="instance.icon && instance.meta?.iconUrl" :src="getInstanceImage(instance)" class="w-full h-full" />
|
||||
<div v-else-if="['forked', 'notDetermined'].includes(getPlaceholderImage(instance))" class="w-full h-full bg-accent-600/20 p-2 text-accent-600">
|
||||
<ForkedIco v-if="getPlaceholderImage(instance) === 'forked'" class="bi h-5 w-5 stroke-1 stroke-current" />
|
||||
<QuestionIco v-else class="h-5 w-5" />
|
||||
</div>
|
||||
<img v-else :src="getPlaceholderImage(instance)" class="w-full h-full" />
|
||||
<div class="pointer-events-none absolute top-0 left-0 w-full h-full bg-white dark:bg-gray-800 transition-opacity opacity-0 group-hover/delete:opacity-100 p-2 text-red-600">
|
||||
<DeleteIco class="h-5 w-5" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<img v-else-if="instance.icon && instance.meta?.iconUrl" :src="instance.meta?.iconUrl" class="w-full h-full" />
|
||||
</div>
|
||||
<div class="min-w-0 mr-3">
|
||||
<h2 class="font-bold truncate">{{ instance.name }}</h2>
|
||||
<p class="text-xs truncate">{{ instance.url }}</p>
|
||||
</div>
|
||||
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4" />
|
||||
</GNuxtLink>
|
||||
</li>
|
||||
<li class="group">
|
||||
<details class="group-first:rounded-t-lg group-last:rounded-b-lg group/details">
|
||||
<summary class="p-4 w-full flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 group-first:rounded-t-lg group-last:rounded-b-lg group-last:group-open/details:rounded-b-none group-open/details:border-b cursor-pointer border-gray-300 dark:border-gray-600">
|
||||
<div class="h-9 w-9 mr-2">
|
||||
<div class="w-full h-full rounded bg-accent-600/20 p-2 text-accent-600">
|
||||
<PlusIco class="h-5 w-5 stroke-1 stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="font-bold">{{ $t('_share.addServer') }}</h2>
|
||||
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4 transition-transform group-open/details:rotate-90" />
|
||||
</summary>
|
||||
<div class="p-4">
|
||||
<form @submit.prevent="getAndSetInstanceInfo">
|
||||
<label class="form-label inline-block" for="userDefinedInstanceInput">{{ $t('_share.domain') }}</label>
|
||||
<div class="input-group">
|
||||
<input id="userDefinedInstanceInput" class="form-control" autocomplete="off" placeholder="misskey.example.com" v-model="userDefinedInstanceInput" :disabled="iFetching" />
|
||||
<button type="submit" class="btn btn-primary !text-white hover:!text-white focus-visible:!text-white" :disabled="iFetching"><PlusIco class="h-4 w-4 stroke-1 stroke-current" /></button>
|
||||
</div>
|
||||
<div class="form-text">{{ $t('_share.compatibleWith') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="text-sm text-center">
|
||||
© 2023 Misskey, syuilo, and other contributors<br>
|
||||
<GNuxtLink to="https://misskey-hub.net/" target="_blank" class="hover:underline underline-offset-1">Misskey Hub</GNuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ShareIco from 'bi/share-fill.svg';
|
||||
import ArrowRightIco from 'bi/chevron-right.svg';
|
||||
import PlusIco from 'bi/plus-lg.svg';
|
||||
import DeleteIco from 'bi/trash.svg';
|
||||
import QuestionIco from 'bi/question-lg.svg';
|
||||
import ForkedIco from '@/assets/svg/repo-forked.svg';
|
||||
import { isLocalPath, resolveObjPath } from '@/assets/js/misc';
|
||||
import { parseURL, joinURL } from 'ufo';
|
||||
import { api as misskeyApi } from 'misskey-js';
|
||||
import { forkedSoftwares } from '~/assets/data/forks';
|
||||
import type { InstanceInfo, InstanceItem } from '@/types/instances-info';
|
||||
import type { FunctionalComponent } from 'vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
path: string;
|
||||
branding?: {
|
||||
heading?: string;
|
||||
icon?: FunctionalComponent | string;
|
||||
};
|
||||
}>();
|
||||
|
||||
type ExtendedInstanceItem = InstanceItem & {
|
||||
isUserDefined?: boolean;
|
||||
};
|
||||
|
||||
const loading = ref(true);
|
||||
const iFetching = ref(false);
|
||||
const featuredInstances = ref<ExtendedInstanceItem[]>([]);
|
||||
const userDefinedInstances = ref<ExtendedInstanceItem[]>([]);
|
||||
const displayInstances = computed(() => [
|
||||
...userDefinedInstances.value,
|
||||
...featuredInstances.value,
|
||||
]);
|
||||
const userDefinedInstanceInput = ref<string>('');
|
||||
|
||||
async function getAndSetInstanceInfo() {
|
||||
if (!process.client || !userDefinedInstanceInput.value || !userDefinedInstanceInput.value.includes('.')) return;
|
||||
iFetching.value = true;
|
||||
|
||||
nextTick(async () => {
|
||||
const realHost = parseURL(userDefinedInstanceInput.value.startsWith('https://') ? userDefinedInstanceInput.value : 'https://' + userDefinedInstanceInput.value);
|
||||
if (!realHost.host) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const miApi = new misskeyApi.APIClient({
|
||||
origin: `https://${realHost.host}`,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await miApi.request('meta', { detail: true });
|
||||
|
||||
userDefinedInstances.value.push({
|
||||
background: !(!res.backgroundImageUrl),
|
||||
banner: !(!res.bannerUrl),
|
||||
description: res.description,
|
||||
icon: !(!res.iconUrl),
|
||||
isAlive: true,
|
||||
langs: res.langs,
|
||||
meta: res,
|
||||
name: res.name ?? '',
|
||||
nodeinfo: null,
|
||||
npd15: 0,
|
||||
stats: {},
|
||||
url: realHost.host ?? '',
|
||||
value: 0,
|
||||
|
||||
isUserDefined: true,
|
||||
});
|
||||
|
||||
userDefinedInstanceInput.value = '';
|
||||
} catch (err) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
console.error(err);
|
||||
} finally {
|
||||
iFetching.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteInstance(host: string) {
|
||||
const i = userDefinedInstances.value.findIndex((v) => v.url === host);
|
||||
userDefinedInstances.value.splice(i, 1);
|
||||
}
|
||||
|
||||
function getInstanceImage(instance: ExtendedInstanceItem | InstanceItem) {
|
||||
if (!instance.meta?.iconUrl) return;
|
||||
|
||||
if (isLocalPath(instance.meta.iconUrl)) {
|
||||
return joinURL(`https://${instance.url}`, instance.meta.iconUrl);
|
||||
}
|
||||
return instance.meta.iconUrl;
|
||||
}
|
||||
|
||||
function getPlaceholderImage(instance: ExtendedInstanceItem | InstanceItem) {
|
||||
if (instance.meta?.repositoryUrl) {
|
||||
if (forkedSoftwares.some((v) => instance.meta?.repositoryUrl.toLowerCase().includes(v))) {
|
||||
return 'forked';
|
||||
}
|
||||
if (instance.meta.repositoryUrl.includes('misskey')) {
|
||||
return '/img/icons/f/mi.png';
|
||||
}
|
||||
}
|
||||
return 'notDetermined';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (process.client) {
|
||||
const fetchedInfo = await window.fetch('https://instanceapp.misskey.page/instances.json');
|
||||
if (![200, 304].includes(fetchedInfo.status)) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
return;
|
||||
}
|
||||
const fetchedInfoJson = await fetchedInfo.json() as InstanceInfo;
|
||||
featuredInstances.value = fetchedInfoJson.instancesInfos.sort((a, b) => {
|
||||
return resolveObjPath(a, 'stats.originalUsersCount') > resolveObjPath(b, 'stats.originalUsersCount') ? -1 : 1;
|
||||
}).slice(0, 5);
|
||||
|
||||
const ls = localStorage.getItem('miHub_share_instances');
|
||||
if (ls) {
|
||||
const lsJ = JSON.parse(ls) as ExtendedInstanceItem[];
|
||||
userDefinedInstances.value = [...lsJ];
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
watch(userDefinedInstances, (to) => {
|
||||
if (process.client) {
|
||||
localStorage.setItem('miHub_share_instances', JSON.stringify(to));
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -2,13 +2,14 @@
|
||||
<NuxtLink
|
||||
:to="realHref"
|
||||
:href="undefined"
|
||||
:target="realTarget"
|
||||
>
|
||||
<slot></slot>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cleanDoubleSlashes, withTrailingSlash } from 'ufo';
|
||||
import { cleanDoubleSlashes, withQuery, withTrailingSlash } from 'ufo';
|
||||
import { isLocalPath, sanitizeInternalPath } from '@/assets/js/misc';
|
||||
import type { RouteLocationRaw } from '#vue-router';
|
||||
|
||||
@ -21,12 +22,20 @@ import type { RouteLocationRaw } from '#vue-router';
|
||||
const rawProps = defineProps<{
|
||||
to?: RouteLocationRaw | string;
|
||||
href?: RouteLocationRaw | string;
|
||||
target?: unknown;
|
||||
}>();
|
||||
|
||||
const localePath = useLocalePath();
|
||||
|
||||
const needsToOpenExternally = ref(false);
|
||||
const realHref = computed(() => {
|
||||
const rhf = rawProps.to ?? rawProps.href;
|
||||
|
||||
if (rhf && typeof rhf === 'string') {
|
||||
if (rhf.startsWith('x-mi-web://')) {
|
||||
needsToOpenExternally.value = true;
|
||||
return localePath(withQuery('/mi-web/', { path: cleanDoubleSlashes(rhf.replace('x-mi-web:/', '')) }));
|
||||
}
|
||||
|
||||
if (isLocalPath(rhf)) {
|
||||
return withTrailingSlash(cleanDoubleSlashes(sanitizeInternalPath(rhf)), true);
|
||||
@ -40,4 +49,6 @@ const realHref = computed(() => {
|
||||
return rhf;
|
||||
});
|
||||
|
||||
const realTarget = computed(() => (needsToOpenExternally.value ? '_blank' : (rawProps.target ?? null)));
|
||||
console.log(realTarget.value);
|
||||
</script>
|
||||
|
@ -273,6 +273,10 @@ _shareLinkGenerator:
|
||||
typeSomethingToGetLink: "本文を入力するとリンクが生成されます。"
|
||||
typeSomethingToGetButton: "本文を入力するとボタンが生成されます。"
|
||||
|
||||
_goToMisskey:
|
||||
title: "Misskey Webに移動"
|
||||
heading: "このページを開きたいサーバーを選択してください"
|
||||
|
||||
_api:
|
||||
_permissions:
|
||||
title: "権限"
|
||||
|
38
pages/mi-web.vue
Normal file
38
pages/mi-web.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<GMisskeyGateway
|
||||
:path="path"
|
||||
:branding="{
|
||||
heading: $t('_goToMisskey.heading'),
|
||||
icon: WindowIco,
|
||||
}"
|
||||
></GMisskeyGateway>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WindowIco from 'bi/window.svg';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'blank',
|
||||
});
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: 'robots', content: 'noindex' },
|
||||
],
|
||||
});
|
||||
|
||||
const { meta, query } = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const path = computed<string>(() => {
|
||||
if (!query.path || query.path.length == 0) return '/';
|
||||
|
||||
if (Array.isArray(query.path)) {
|
||||
return query.path[0] as string;
|
||||
} else {
|
||||
return query.path;
|
||||
}
|
||||
})
|
||||
|
||||
meta.title = t('_goToMisskey.title');
|
||||
</script>
|
214
pages/share.vue
214
pages/share.vue
@ -1,93 +1,11 @@
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<div class="sm:py-20 sm:max-w-xl mx-auto">
|
||||
<div class="p-6 bg-white dark:bg-gray-800 sm:rounded-lg min-h-screen sm:min-h-[230px]">
|
||||
<div v-if="loading" class="mx-auto py-12">
|
||||
<MkLoading />
|
||||
</div>
|
||||
<div v-else class="space-y-6">
|
||||
<div class="w-12 h-12 mx-auto bg-accent-600/20 text-accent-600 rounded-full p-3">
|
||||
<ShareIco class="w-6 h-6" />
|
||||
</div>
|
||||
<h1 class="font-bold text-center text-lg md:text-xl">{{ $t('_share.chooseServer') }}<div class="inline-block ml-1 text-sm px-2 bg-orange-100 text-orange-600 rounded-full">BETA</div></h1>
|
||||
<ul class="rounded-lg border divide-y border-gray-300 dark:border-gray-600 divide-gray-300 dark:divide-gray-600">
|
||||
<li v-for="instance in displayInstances" :key="instance.url" class="group">
|
||||
<GNuxtLink
|
||||
:to="`https://${instance.url}/share?${stringifyQuery(query)}`"
|
||||
class="group-first:rounded-t-lg group-last:rounded-b-lg p-4 w-full flex items-center hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-600"
|
||||
>
|
||||
<div
|
||||
class="h-9 w-9 flex-shrink-0 overflow-hidden rounded bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 mr-3"
|
||||
:class="instance.isUserDefined && 'transition-[border-color] hover:border-red-600 dark:hover:border-red-600'"
|
||||
>
|
||||
<template v-if="instance.isUserDefined">
|
||||
<button class="relative w-full h-full group/delete" @click.stop.prevent="deleteInstance(instance.url)">
|
||||
<img v-if="instance.icon && instance.meta?.iconUrl" :src="getInstanceImage(instance)" class="w-full h-full" />
|
||||
<div v-else-if="['forked', 'notDetermined'].includes(getPlaceholderImage(instance))" class="w-full h-full bg-accent-600/20 p-2 text-accent-600">
|
||||
<ForkedIco v-if="getPlaceholderImage(instance) === 'forked'" class="bi h-5 w-5 stroke-1 stroke-current" />
|
||||
<QuestionIco v-else class="h-5 w-5" />
|
||||
</div>
|
||||
<img v-else :src="getPlaceholderImage(instance)" class="w-full h-full" />
|
||||
<div class="pointer-events-none absolute top-0 left-0 w-full h-full bg-white dark:bg-gray-800 transition-opacity opacity-0 group-hover/delete:opacity-100 p-2 text-red-600">
|
||||
<DeleteIco class="h-5 w-5" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<img v-else-if="instance.icon && instance.meta?.iconUrl" :src="instance.meta?.iconUrl" class="w-full h-full" />
|
||||
</div>
|
||||
<div class="min-w-0 mr-3">
|
||||
<h2 class="font-bold truncate">{{ instance.name }}</h2>
|
||||
<p class="text-xs truncate">{{ instance.url }}</p>
|
||||
</div>
|
||||
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4" />
|
||||
</GNuxtLink>
|
||||
</li>
|
||||
<li class="group">
|
||||
<details class="group-first:rounded-t-lg group-last:rounded-b-lg group/details">
|
||||
<summary class="p-4 w-full flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 group-first:rounded-t-lg group-last:rounded-b-lg group-last:group-open/details:rounded-b-none group-open/details:border-b cursor-pointer border-gray-300 dark:border-gray-600">
|
||||
<div class="h-9 w-9 mr-2">
|
||||
<div class="w-full h-full rounded bg-accent-600/20 p-2 text-accent-600">
|
||||
<PlusIco class="h-5 w-5 stroke-1 stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="font-bold">{{ $t('_share.addServer') }}</h2>
|
||||
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4 transition-transform group-open/details:rotate-90" />
|
||||
</summary>
|
||||
<div class="p-4">
|
||||
<form @submit.prevent="getAndSetInstanceInfo">
|
||||
<label class="form-label inline-block" for="userDefinedInstanceInput">{{ $t('_share.domain') }}</label>
|
||||
<div class="input-group">
|
||||
<input id="userDefinedInstanceInput" class="form-control" autocomplete="off" placeholder="misskey.example.com" v-model="userDefinedInstanceInput" :disabled="iFetching" />
|
||||
<button type="submit" class="btn btn-primary !text-white hover:!text-white focus-visible:!text-white" :disabled="iFetching"><PlusIco class="h-4 w-4 stroke-1 stroke-current" /></button>
|
||||
</div>
|
||||
<div class="form-text">{{ $t('_share.compatibleWith') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="text-sm text-center">
|
||||
© 2023 Misskey, syuilo, and other contributors<br>
|
||||
<GNuxtLink to="https://misskey-hub.net/" target="_blank" class="hover:underline underline-offset-1">Misskey Hub</GNuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GMisskeyGateway
|
||||
:path="`/share?${stringifyQuery(query)}`"
|
||||
></GMisskeyGateway>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ShareIco from 'bi/share-fill.svg';
|
||||
import ArrowRightIco from 'bi/chevron-right.svg';
|
||||
import PlusIco from 'bi/plus-lg.svg';
|
||||
import DeleteIco from 'bi/trash.svg';
|
||||
import QuestionIco from 'bi/question-lg.svg';
|
||||
import ForkedIco from '@/assets/svg/repo-forked.svg';
|
||||
import { isLocalPath, resolveObjPath } from '@/assets/js/misc';
|
||||
import { stringifyQuery, parseURL, joinURL } from 'ufo';
|
||||
import { api as misskeyApi } from 'misskey-js';
|
||||
import { forkedSoftwares } from '~/assets/data/forks';
|
||||
import type { InstanceInfo, InstanceItem } from '@/types/instances-info';
|
||||
import { stringifyQuery } from 'ufo';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'blank',
|
||||
@ -99,128 +17,8 @@ useHead({
|
||||
],
|
||||
});
|
||||
|
||||
const { query, meta } = useRoute();
|
||||
const { meta, query } = useRoute();
|
||||
const { t } = useI18n();
|
||||
const localePath = useLocalePath();
|
||||
|
||||
meta.title = t('_share.title');
|
||||
|
||||
type ExtendedInstanceItem = InstanceItem & {
|
||||
isUserDefined?: boolean;
|
||||
};
|
||||
|
||||
const loading = ref(true);
|
||||
const iFetching = ref(false);
|
||||
const featuredInstances = ref<ExtendedInstanceItem[]>([]);
|
||||
const userDefinedInstances = ref<ExtendedInstanceItem[]>([]);
|
||||
const displayInstances = computed(() => [
|
||||
...userDefinedInstances.value,
|
||||
...featuredInstances.value,
|
||||
]);
|
||||
const userDefinedInstanceInput = ref<string>('');
|
||||
|
||||
async function getAndSetInstanceInfo() {
|
||||
if (!process.client || !userDefinedInstanceInput.value || !userDefinedInstanceInput.value.includes('.')) return;
|
||||
iFetching.value = true;
|
||||
|
||||
nextTick(async () => {
|
||||
const realHost = parseURL(userDefinedInstanceInput.value.startsWith('https://') ? userDefinedInstanceInput.value : 'https://' + userDefinedInstanceInput.value);
|
||||
if (!realHost.host) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const miApi = new misskeyApi.APIClient({
|
||||
origin: `https://${realHost.host}`,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await miApi.request('meta', { detail: true });
|
||||
|
||||
userDefinedInstances.value.push({
|
||||
background: !(!res.backgroundImageUrl),
|
||||
banner: !(!res.bannerUrl),
|
||||
description: res.description,
|
||||
icon: !(!res.iconUrl),
|
||||
isAlive: true,
|
||||
langs: res.langs,
|
||||
meta: res,
|
||||
name: res.name ?? '',
|
||||
nodeinfo: null,
|
||||
stats: {},
|
||||
url: realHost.host ?? '',
|
||||
value: 0,
|
||||
|
||||
isUserDefined: true,
|
||||
});
|
||||
|
||||
userDefinedInstanceInput.value = '';
|
||||
} catch (err) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
console.error(err);
|
||||
} finally {
|
||||
iFetching.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteInstance(host: string) {
|
||||
const i = userDefinedInstances.value.findIndex((v) => v.url === host);
|
||||
userDefinedInstances.value.splice(i, 1);
|
||||
}
|
||||
|
||||
function getInstanceImage(instance: ExtendedInstanceItem | InstanceItem) {
|
||||
if (!instance.meta?.iconUrl) return;
|
||||
|
||||
if (isLocalPath(instance.meta.iconUrl)) {
|
||||
return joinURL(`https://${instance.url}`, instance.meta.iconUrl);
|
||||
}
|
||||
return instance.meta.iconUrl;
|
||||
}
|
||||
|
||||
function getPlaceholderImage(instance: ExtendedInstanceItem | InstanceItem) {
|
||||
if (instance.meta?.repositoryUrl) {
|
||||
if (forkedSoftwares.some((v) => instance.meta?.repositoryUrl.toLowerCase().includes(v))) {
|
||||
return 'forked';
|
||||
}
|
||||
if (instance.meta.repositoryUrl.includes('misskey')) {
|
||||
return '/img/icons/f/mi.png';
|
||||
}
|
||||
}
|
||||
return 'notDetermined';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (process.client) {
|
||||
const fetchedInfo = await window.fetch('https://instanceapp.misskey.page/instances.json');
|
||||
if (![200, 304].includes(fetchedInfo.status)) {
|
||||
alert(t('_servers._system.fetchError'));
|
||||
return;
|
||||
}
|
||||
const fetchedInfoJson = await fetchedInfo.json() as InstanceInfo;
|
||||
featuredInstances.value = fetchedInfoJson.instancesInfos.sort((a, b) => {
|
||||
return resolveObjPath(a, 'stats.originalUsersCount') > resolveObjPath(b, 'stats.originalUsersCount') ? -1 : 1;
|
||||
}).slice(0, 5);
|
||||
|
||||
const ls = localStorage.getItem('miHub_share_instances');
|
||||
if (ls) {
|
||||
const lsJ = JSON.parse(ls) as ExtendedInstanceItem[];
|
||||
userDefinedInstances.value = [...lsJ];
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
watch(userDefinedInstances, (to) => {
|
||||
if (process.client) {
|
||||
localStorage.setItem('miHub_share_instances', JSON.stringify(to));
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user