Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
commit
fd696a9621
113 changed files with 2272 additions and 443 deletions
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div class="detail">
|
||||
<div>
|
||||
<Mfm :text="report.comment"/>
|
||||
<Mfm :text="report.comment" :linkBehavior="'window'"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-else class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:to="to ?? '#'"
|
||||
:behavior="linkBehavior"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
|
||||
|
@ -43,6 +44,7 @@ const props = defineProps<{
|
|||
inline?: boolean;
|
||||
link?: boolean;
|
||||
to?: string;
|
||||
linkBehavior?: null | 'window' | 'browser';
|
||||
autofocus?: boolean;
|
||||
wait?: boolean;
|
||||
danger?: boolean;
|
||||
|
|
|
@ -4,37 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" class="_panel">
|
||||
<b>{{ clip.name }}</b>
|
||||
<div v-if="clip.description" :class="$style.description">{{ clip.description }}</div>
|
||||
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
|
||||
<div :class="$style.user">
|
||||
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
<MkA :to="`/clips/${clip.id}`" :class="$style.link">
|
||||
<div :class="$style.root" class="_panel _gaps_s">
|
||||
<b>{{ clip.name }}</b>
|
||||
<div :class="$style.description">
|
||||
<div v-if="clip.description"><Mfm :text="clip.description" :plain="true" :nowrap="true"/></div>
|
||||
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
|
||||
<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
|
||||
</div>
|
||||
<div :class="$style.divider"></div>
|
||||
<div>
|
||||
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
||||
defineProps<{
|
||||
clip: any;
|
||||
const props = defineProps<{
|
||||
clip: Misskey.entities.Clip;
|
||||
}>();
|
||||
|
||||
const remaining = computed(() => {
|
||||
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
.link {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 8px 0;
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
.user {
|
||||
padding-top: 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
.description {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
|
|
|
@ -204,7 +204,7 @@ function onKeydown(evt: KeyboardEvent) {
|
|||
}
|
||||
|
||||
function onInputKeydown(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Enter') {
|
||||
if (evt.key === 'Enter' && !okDisabled.value && okButtonDisabledReason.value === null) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
ok();
|
||||
|
|
|
@ -94,6 +94,18 @@ async function onClick() {
|
|||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (defaultStore.state.alwaysConfirmFollow) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
wait.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (userDetailed.value.hasPendingFollowRequestFromYou) {
|
||||
await misskeyApi('following/requests/cancel', {
|
||||
userId: props.user.id,
|
||||
|
|
|
@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:[attr]="self ? url.substring(local.length) : url"
|
||||
:rel="rel ?? 'nofollow noopener'"
|
||||
:target="target"
|
||||
:behavior="props.behavior"
|
||||
:title="url"
|
||||
@click="(ev: MouseEvent) => warningExternalWebsite(ev, url)"
|
||||
>
|
||||
|
@ -27,10 +28,12 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
|
|||
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||
import * as os from '@/os.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
rel?: null | string;
|
||||
behavior?: MkABehavior;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
|
|
|
@ -5,11 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
ref="playerEl"
|
||||
v-hotkey="keymap"
|
||||
tabindex="0"
|
||||
:class="[
|
||||
$style.audioContainer,
|
||||
(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
|
||||
]"
|
||||
@contextmenu.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<button v-if="hide" :class="$style.hidden" @click="showHiddenContent">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
|
@ -18,6 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer">
|
||||
<audio
|
||||
ref="audioEl"
|
||||
preload="metadata"
|
||||
controls
|
||||
:class="$style.nativeAudio"
|
||||
@keydown.prevent
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$style.audioControls">
|
||||
<audio
|
||||
ref="audioEl"
|
||||
|
@ -73,6 +90,41 @@ const props = defineProps<{
|
|||
audio: Misskey.entities.DriveFile;
|
||||
}>();
|
||||
|
||||
const keymap = {
|
||||
'up': () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
volume.value = Math.min(volume.value + 0.1, 1);
|
||||
}
|
||||
},
|
||||
'down': () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
volume.value = Math.max(volume.value - 0.1, 0);
|
||||
}
|
||||
},
|
||||
'left': () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
|
||||
}
|
||||
},
|
||||
'right': () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
|
||||
}
|
||||
},
|
||||
'space': () => {
|
||||
if (hasFocus()) {
|
||||
togglePlayPause();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||
function hasFocus() {
|
||||
if (!playerEl.value) return false;
|
||||
return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
|
||||
}
|
||||
|
||||
const playerEl = shallowRef<HTMLDivElement>();
|
||||
const audioEl = shallowRef<HTMLAudioElement>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
|
@ -84,6 +136,30 @@ const menuShowing = ref(false);
|
|||
function showMenu(ev: MouseEvent) {
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts._mediaControls.loop,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: loop,
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
text: i18n.ts._mediaControls.playbackRate,
|
||||
icon: 'ti ti-clock-play',
|
||||
ref: speed,
|
||||
options: {
|
||||
'0.25x': 0.25,
|
||||
'0.5x': 0.5,
|
||||
'0.75x': 0.75,
|
||||
'1.0x': 1,
|
||||
'1.25x': 1.25,
|
||||
'1.5x': 1.5,
|
||||
'2.0x': 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
|
@ -174,6 +250,8 @@ const rangePercent = computed({
|
|||
},
|
||||
});
|
||||
const volume = ref(.25);
|
||||
const speed = ref(1);
|
||||
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
|
||||
const bufferedEnd = ref(0);
|
||||
const bufferedDataRatio = computed(() => {
|
||||
if (!audioEl.value) return 0;
|
||||
|
@ -203,6 +281,7 @@ function toggleMute() {
|
|||
}
|
||||
|
||||
let onceInit = false;
|
||||
let mediaTickFrameId: number | null = null;
|
||||
let stopAudioElWatch: () => void;
|
||||
|
||||
function init() {
|
||||
|
@ -222,8 +301,12 @@ function init() {
|
|||
}
|
||||
|
||||
elapsedTimeMs.value = audioEl.value.currentTime * 1000;
|
||||
|
||||
if (audioEl.value.loop !== loop.value) {
|
||||
loop.value = audioEl.value.loop;
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(updateMediaTick);
|
||||
mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
|
||||
}
|
||||
|
||||
updateMediaTick();
|
||||
|
@ -261,6 +344,14 @@ watch(volume, (to) => {
|
|||
if (audioEl.value) audioEl.value.volume = to;
|
||||
});
|
||||
|
||||
watch(speed, (to) => {
|
||||
if (audioEl.value) audioEl.value.playbackRate = to;
|
||||
});
|
||||
|
||||
watch(loop, (to) => {
|
||||
if (audioEl.value) audioEl.value.loop = to;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
@ -279,6 +370,10 @@ onDeactivated(() => {
|
|||
hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||
stopAudioElWatch();
|
||||
onceInit = false;
|
||||
if (mediaTickFrameId) {
|
||||
window.cancelAnimationFrame(mediaTickFrameId);
|
||||
mediaTickFrameId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -289,6 +384,10 @@ onDeactivated(() => {
|
|||
border: .5px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
|
@ -394,4 +493,15 @@ onDeactivated(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nativeAudioContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.nativeAudio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div
|
||||
ref="playerEl"
|
||||
v-hotkey="keymap"
|
||||
tabindex="0"
|
||||
:class="[
|
||||
$style.videoContainer,
|
||||
controlsShowing && $style.active,
|
||||
|
@ -14,15 +16,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@mouseover="onMouseOver"
|
||||
@mouseleave="onMouseLeave"
|
||||
@contextmenu.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<button v-if="hide" :class="$style.hidden" @click="showHiddenContent">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div v-else :class="$style.videoRoot" @click.self="togglePlayPause">
|
||||
|
||||
<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot">
|
||||
<video
|
||||
ref="videoEl"
|
||||
:class="$style.video"
|
||||
:poster="video.thumbnailUrl ?? undefined"
|
||||
:title="video.comment ?? undefined"
|
||||
:alt="video.comment"
|
||||
preload="metadata"
|
||||
controls
|
||||
@keydown.prevent
|
||||
>
|
||||
<source :src="video.url">
|
||||
</video>
|
||||
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="video.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$style.videoRoot">
|
||||
<video
|
||||
ref="videoEl"
|
||||
:class="$style.video"
|
||||
|
@ -31,6 +55,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:alt="video.comment"
|
||||
preload="metadata"
|
||||
playsinline
|
||||
@keydown.prevent
|
||||
@click.self="togglePlayPause"
|
||||
>
|
||||
<source :src="video.url">
|
||||
</video>
|
||||
|
@ -105,6 +131,40 @@ const props = withDefaults(defineProps<{
|
|||
videoControls: true,
|
||||
});
|
||||
|
||||
const keymap = {
|
||||
'up': () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
volume.value = Math.min(volume.value + 0.1, 1);
|
||||
}
|
||||
},
|
||||
'down': () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
volume.value = Math.max(volume.value - 0.1, 0);
|
||||
}
|
||||
},
|
||||
'left': () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
|
||||
}
|
||||
},
|
||||
'right': () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
|
||||
}
|
||||
},
|
||||
'space': () => {
|
||||
if (hasFocus()) {
|
||||
togglePlayPause();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||
function hasFocus() {
|
||||
if (!playerEl.value) return false;
|
||||
return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||
|
||||
|
@ -114,6 +174,35 @@ const menuShowing = ref(false);
|
|||
function showMenu(ev: MouseEvent) {
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts._mediaControls.loop,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: loop,
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
text: i18n.ts._mediaControls.playbackRate,
|
||||
icon: 'ti ti-clock-play',
|
||||
ref: speed,
|
||||
options: {
|
||||
'0.25x': 0.25,
|
||||
'0.5x': 0.5,
|
||||
'0.75x': 0.75,
|
||||
'1.0x': 1,
|
||||
'1.25x': 1.25,
|
||||
'1.5x': 1.5,
|
||||
'2.0x': 2,
|
||||
},
|
||||
},
|
||||
...(document.pictureInPictureEnabled ? [{
|
||||
text: i18n.ts._mediaControls.pip,
|
||||
icon: 'ti ti-picture-in-picture',
|
||||
action: togglePictureInPicture,
|
||||
}] : []),
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
|
@ -217,6 +306,8 @@ const rangePercent = computed({
|
|||
},
|
||||
});
|
||||
const volume = ref(.25);
|
||||
const speed = ref(1);
|
||||
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
|
||||
const bufferedEnd = ref(0);
|
||||
const bufferedDataRatio = computed(() => {
|
||||
if (!videoEl.value) return 0;
|
||||
|
@ -274,6 +365,16 @@ function toggleFullscreen() {
|
|||
}
|
||||
}
|
||||
|
||||
function togglePictureInPicture() {
|
||||
if (videoEl.value) {
|
||||
if (document.pictureInPictureElement) {
|
||||
document.exitPictureInPicture();
|
||||
} else {
|
||||
videoEl.value.requestPictureInPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (volume.value === 0) {
|
||||
volume.value = .25;
|
||||
|
@ -283,6 +384,7 @@ function toggleMute() {
|
|||
}
|
||||
|
||||
let onceInit = false;
|
||||
let mediaTickFrameId: number | null = null;
|
||||
let stopVideoElWatch: () => void;
|
||||
|
||||
function init() {
|
||||
|
@ -302,8 +404,12 @@ function init() {
|
|||
}
|
||||
|
||||
elapsedTimeMs.value = videoEl.value.currentTime * 1000;
|
||||
|
||||
if (videoEl.value.loop !== loop.value) {
|
||||
loop.value = videoEl.value.loop;
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(updateMediaTick);
|
||||
mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
|
||||
}
|
||||
|
||||
updateMediaTick();
|
||||
|
@ -347,6 +453,14 @@ watch(volume, (to) => {
|
|||
if (videoEl.value) videoEl.value.volume = to;
|
||||
});
|
||||
|
||||
watch(speed, (to) => {
|
||||
if (videoEl.value) videoEl.value.playbackRate = to;
|
||||
});
|
||||
|
||||
watch(loop, (to) => {
|
||||
if (videoEl.value) videoEl.value.loop = to;
|
||||
});
|
||||
|
||||
watch(hide, (to) => {
|
||||
if (to && isFullscreen.value) {
|
||||
document.exitFullscreen();
|
||||
|
@ -372,6 +486,10 @@ onDeactivated(() => {
|
|||
hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||
stopVideoElWatch();
|
||||
onceInit = false;
|
||||
if (mediaTickFrameId) {
|
||||
window.cancelAnimationFrame(mediaTickFrameId);
|
||||
mediaTickFrameId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -380,6 +498,10 @@ onDeactivated(() => {
|
|||
container-type: inline-size;
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
|
@ -482,7 +604,6 @@ onDeactivated(() => {
|
|||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.videoOverlayPlayButton {
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="behavior">
|
||||
<img :class="$style.icon" :src="avatarUrl" alt="">
|
||||
<span>
|
||||
<span>@{{ username }}</span>
|
||||
|
@ -21,10 +21,12 @@ import { host as localHost } from '@/config.js';
|
|||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
behavior?: MkABehavior;
|
||||
}>();
|
||||
|
||||
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
|
||||
|
|
|
@ -42,9 +42,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
|
||||
<span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span>
|
||||
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<div :class="$style.icon">
|
||||
<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
|
||||
</div>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
||||
|
@ -77,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
|
@ -168,6 +185,31 @@ function onItemMouseLeave(item) {
|
|||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||
}
|
||||
|
||||
async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
||||
const value = item.options[key];
|
||||
return {
|
||||
type: 'radioOption',
|
||||
text: key,
|
||||
action: () => {
|
||||
item.ref = value;
|
||||
},
|
||||
active: computed(() => item.ref === value),
|
||||
};
|
||||
});
|
||||
|
||||
if (props.asDrawer) {
|
||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||
emit('close');
|
||||
});
|
||||
emit('hide');
|
||||
} else {
|
||||
childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
|
||||
childMenu.value = children;
|
||||
childShowingItem.value = item;
|
||||
}
|
||||
}
|
||||
|
||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||
const children: MenuItem[] = await (async () => {
|
||||
if (childrenCache.has(item)) {
|
||||
|
@ -196,8 +238,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
function clicked(fn: MenuAction, ev: MouseEvent) {
|
||||
function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
|
||||
fn(ev);
|
||||
|
||||
if (!doClose) return;
|
||||
close(true);
|
||||
}
|
||||
|
||||
|
@ -350,6 +394,15 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
}
|
||||
|
||||
&.radioActive {
|
||||
color: var(--accent) !important;
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
background-color: var(--accentedBg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active):focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
||||
}
|
||||
|
@ -417,11 +470,11 @@ onBeforeUnmount(() => {
|
|||
|
||||
.switchButton {
|
||||
margin-left: -2px;
|
||||
--height: 1.35em;
|
||||
}
|
||||
|
||||
.switchText {
|
||||
margin-left: 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -461,4 +514,32 @@ onBeforeUnmount(() => {
|
|||
margin: 8px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -.125em;
|
||||
border-radius: 50%;
|
||||
border: solid 2px var(--divider);
|
||||
background-color: var(--panel);
|
||||
|
||||
&.radioChecked {
|
||||
border-color: var(--accent);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -237,6 +237,7 @@ if (noteViewInterruptors.length > 0) {
|
|||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
|
|
|
@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
|
@ -267,7 +267,9 @@ if (noteViewInterruptors.length > 0) {
|
|||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
note.value.poll == null
|
||||
);
|
||||
|
|
|
@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
|
@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote?.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
|
@ -176,6 +176,11 @@ const rejectFollowRequest = () => {
|
|||
followRequestDone.value = true;
|
||||
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
|
||||
};
|
||||
|
||||
function getActualReactedUsersCount(notification: Misskey.entities.Notification) {
|
||||
if (notification.type !== 'reaction:grouped') return 0;
|
||||
return new Set(notification.reactions.map((reaction) => reaction.user.id)).size;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -260,7 +260,13 @@ const maxTextLength = computed((): number => {
|
|||
|
||||
const canPost = computed((): boolean => {
|
||||
return !props.mock && !posting.value && !posted.value &&
|
||||
(1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
|
||||
(
|
||||
1 <= textLength.value ||
|
||||
1 <= files.value.length ||
|
||||
poll.value != null ||
|
||||
props.renote != null ||
|
||||
(props.reply != null && quoteId.value != null)
|
||||
) &&
|
||||
(textLength.value <= maxTextLength.value) &&
|
||||
(!poll.value || poll.value.choices.length >= 2);
|
||||
});
|
||||
|
@ -389,7 +395,7 @@ function addMissingMention() {
|
|||
for (const x of extractMentions(ast)) {
|
||||
if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
|
||||
misskeyApi('users/show', { username: x.username, host: x.host }).then(user => {
|
||||
visibleUsers.value.push(user);
|
||||
pushVisibleUser(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -682,6 +688,7 @@ function saveDraft() {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
files: files.value.filter(f => f?.id && f.type && f.name),
|
||||
poll: poll.value,
|
||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -958,6 +965,15 @@ onMounted(() => {
|
|||
if (draft.data.poll) {
|
||||
poll.value = draft.data.poll;
|
||||
}
|
||||
if (draft.data.visibleUserIds) {
|
||||
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].id === draft.data.visibleUserIds[i]) {
|
||||
pushVisibleUser(users[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
|||
.root {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 4px -2px 0 -2px;
|
||||
|
||||
&:empty {
|
||||
|
|
|
@ -41,13 +41,15 @@ const toggle = () => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
--height: 21px;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: 32px;
|
||||
height: 23px;
|
||||
width: calc(var(--height) * 1.6);
|
||||
height: calc(var(--height) + 2px); // 枠線
|
||||
outline: none;
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
|
@ -69,9 +71,10 @@ const toggle = () => {
|
|||
|
||||
.knob {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: calc(var(--height) - 6px);
|
||||
height: calc(var(--height) - 6px);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
|
@ -82,7 +85,7 @@ const toggle = () => {
|
|||
}
|
||||
|
||||
.knobChecked {
|
||||
left: 12px;
|
||||
left: calc(calc(100% - var(--height)) + 3px);
|
||||
background: var(--switchOnFg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type MkABehavior = 'window' | 'browser' | null;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -20,12 +24,14 @@ import { useRouter } from '@/router/supplier.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
activeClass?: null | string;
|
||||
behavior?: null | 'window' | 'browser';
|
||||
behavior?: MkABehavior;
|
||||
}>(), {
|
||||
activeClass: null,
|
||||
behavior: null,
|
||||
});
|
||||
|
||||
const linkBehaviour = props.behavior;
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
|
||||
defineExpose({ $el: el });
|
||||
|
|
|
@ -16,7 +16,7 @@ import MkCode from '@/components/MkCode.vue';
|
|||
import MkCodeInline from '@/components/MkCodeInline.vue';
|
||||
import MkGoogle from '@/components/MkGoogle.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import MkA from '@/components/global/MkA.vue';
|
||||
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||
|
@ -43,6 +43,7 @@ type MfmProps = {
|
|||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
linkBehavior?: MkABehavior;
|
||||
};
|
||||
|
||||
type MfmEvents = {
|
||||
|
@ -342,6 +343,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
key: Math.random(),
|
||||
url: token.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
behavior: props.linkBehavior,
|
||||
})];
|
||||
}
|
||||
|
||||
|
@ -350,6 +352,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
key: Math.random(),
|
||||
url: token.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
behavior: props.linkBehavior,
|
||||
}, genEl(token.children, scale, true))];
|
||||
}
|
||||
|
||||
|
@ -358,6 +361,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
key: Math.random(),
|
||||
host: (token.props.host == null && props.author?.host ? props.author.host : token.props.host) ?? host,
|
||||
username: token.props.username,
|
||||
behavior: props.linkBehavior,
|
||||
})];
|
||||
}
|
||||
|
||||
|
@ -366,6 +370,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
key: Math.random(),
|
||||
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
|
||||
style: 'color:var(--hashtag);',
|
||||
behavior: props.linkBehavior,
|
||||
}, `#${token.props.hashtag}`)];
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ const invalid = Number.isNaN(_time);
|
|||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
const now = ref((props.origin ?? new Date()).getTime());
|
||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
||||
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
|
||||
|
||||
const relative = computed<string>(() => {
|
||||
|
@ -77,7 +77,7 @@ let tickId: number;
|
|||
let currentInterval: number;
|
||||
|
||||
function tick() {
|
||||
now.value = (new Date()).getTime();
|
||||
now.value = Date.now();
|
||||
const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
|
||||
|
||||
if (currentInterval !== nextInterval) {
|
||||
|
|
|
@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:[attr]="self ? props.url.substring(local.length) : props.url"
|
||||
:rel="rel ?? 'nofollow noopener'"
|
||||
:target="target"
|
||||
:behavior = "props.behavior"
|
||||
@click="(ev: MouseEvent) => warningExternalWebsite(ev, props.url)"
|
||||
@contextmenu.stop="() => {}"
|
||||
>
|
||||
|
@ -39,11 +40,13 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
|
|||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
rel?: string;
|
||||
showUrlPreview?: boolean;
|
||||
behavior?: MkABehavior;
|
||||
}>(), {
|
||||
showUrlPreview: true,
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import XText from './page.text.vue';
|
|||
import XSection from './page.section.vue';
|
||||
import XImage from './page.image.vue';
|
||||
import XNote from './page.note.vue';
|
||||
import XDynamic from './page.dynamic.vue';
|
||||
|
||||
function getComponent(type: string) {
|
||||
switch (type) {
|
||||
|
@ -21,6 +22,20 @@ function getComponent(type: string) {
|
|||
case 'section': return XSection;
|
||||
case 'image': return XImage;
|
||||
case 'note': return XNote;
|
||||
|
||||
// 動的ページの代替用ブロック
|
||||
case 'button':
|
||||
case 'if':
|
||||
case 'textarea':
|
||||
case 'post':
|
||||
case 'canvas':
|
||||
case 'numberInput':
|
||||
case 'textInput':
|
||||
case 'switch':
|
||||
case 'radioButton':
|
||||
case 'counter':
|
||||
return XDynamic;
|
||||
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
43
packages/frontend/src/components/page/page.dynamic.vue
Normal file
43
packages/frontend/src/components/page/page.dynamic.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<!-- 動的ページのブロックの代替。利用できないということを表示する -->
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.heading"><i class="ti ti-dice-5"></i> {{ i18n.ts._pages.blocks.dynamic }}</div>
|
||||
<I18n :src="i18n.ts._pages.blocks.dynamicDescription" tag="div" :class="$style.text">
|
||||
<template #play>
|
||||
<MkA to="/play" class="_link">Play</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
block: Misskey.entities.PageBlock,
|
||||
page: Misskey.entities.Page,
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--margin);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 90%;
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps" :class="$style.textRoot">
|
||||
<Mfm :text="block.text ?? ''" :isNote="false"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue