Merge branch 'develop' into mahjong
This commit is contained in:
@@ -53,7 +53,6 @@ await fs.readFile(
|
||||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/ja-JP.yml',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
|
@@ -24,12 +24,12 @@
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.18.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.3.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
"@vue/compiler-sfc": "3.4.31",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
|
||||
"@vitejs/plugin-vue": "5.1.0",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"buraha": "0.0.1",
|
||||
@@ -39,9 +39,9 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "11.5.4",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.5",
|
||||
"chromatic": "11.5.6",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0-rc.1",
|
||||
"date-fns": "2.30.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
@@ -58,85 +58,85 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.18.0",
|
||||
"rollup": "4.19.1",
|
||||
"sanitize-html": "2.13.0",
|
||||
"sass": "1.77.6",
|
||||
"shiki": "1.10.0",
|
||||
"sass": "1.77.8",
|
||||
"shiki": "1.12.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.165.0",
|
||||
"three": "0.167.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.5.3",
|
||||
"typescript": "5.5.4",
|
||||
"uuid": "10.0.0",
|
||||
"v-code-diff": "1.12.0",
|
||||
"vite": "5.3.2",
|
||||
"vue": "3.4.31",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-essentials": "8.1.11",
|
||||
"@storybook/addon-interactions": "8.1.11",
|
||||
"@storybook/addon-links": "8.1.11",
|
||||
"@storybook/addon-mdx-gfm": "8.1.11",
|
||||
"@storybook/addon-storysource": "8.1.11",
|
||||
"@storybook/blocks": "8.1.11",
|
||||
"@storybook/components": "8.1.11",
|
||||
"@storybook/core-events": "8.1.11",
|
||||
"@storybook/manager-api": "8.1.11",
|
||||
"@storybook/preview-api": "8.1.11",
|
||||
"@storybook/react": "8.1.11",
|
||||
"@storybook/react-vite": "8.1.11",
|
||||
"@storybook/test": "8.1.11",
|
||||
"@storybook/theming": "8.1.11",
|
||||
"@storybook/types": "8.1.11",
|
||||
"@storybook/vue3": "8.1.11",
|
||||
"@storybook/addon-actions": "8.2.6",
|
||||
"@storybook/addon-essentials": "8.2.6",
|
||||
"@storybook/addon-interactions": "8.2.6",
|
||||
"@storybook/addon-links": "8.2.6",
|
||||
"@storybook/addon-mdx-gfm": "8.2.6",
|
||||
"@storybook/addon-storysource": "8.2.6",
|
||||
"@storybook/blocks": "8.2.6",
|
||||
"@storybook/components": "8.2.6",
|
||||
"@storybook/core-events": "8.2.6",
|
||||
"@storybook/manager-api": "8.2.6",
|
||||
"@storybook/preview-api": "8.2.6",
|
||||
"@storybook/react": "8.2.6",
|
||||
"@storybook/react-vite": "8.2.6",
|
||||
"@storybook/test": "8.2.6",
|
||||
"@storybook/theming": "8.2.6",
|
||||
"@storybook/types": "8.2.6",
|
||||
"@storybook/vue3": "8.2.6",
|
||||
"@storybook/vue3-vite": "8.1.11",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/matter-js": "0.19.7",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/punycode": "2.1.4",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@types/ws": "8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/runtime-core": "3.4.31",
|
||||
"acorn": "8.12.0",
|
||||
"@vue/runtime-core": "3.4.34",
|
||||
"acorn": "8.12.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.0",
|
||||
"cypress": "13.13.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-vue": "9.26.0",
|
||||
"eslint-plugin-vue": "9.27.0",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.7",
|
||||
"msw": "2.3.1",
|
||||
"msw-storybook-addon": "2.0.2",
|
||||
"msw": "2.3.4",
|
||||
"msw-storybook-addon": "2.0.3",
|
||||
"nodemon": "3.1.4",
|
||||
"prettier": "3.3.2",
|
||||
"prettier": "3.3.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.0.4",
|
||||
"storybook": "8.1.11",
|
||||
"storybook": "8.2.6",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "1.6.0",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-component-type-helpers": "2.0.24",
|
||||
"vue-component-type-helpers": "2.0.29",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.0.24"
|
||||
"vue-tsc": "2.0.29"
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { ui } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||
@@ -13,7 +14,6 @@ import * as sound from '@/scripts/sound.js';
|
||||
import { $i, signout, updateAccount } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||
@@ -21,6 +21,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
@@ -69,14 +70,6 @@ export async function mainBoot() {
|
||||
});
|
||||
}
|
||||
|
||||
const hotkeys = {
|
||||
'd': (): void => {
|
||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||
},
|
||||
's': (): void => {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
};
|
||||
try {
|
||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||
const month = new Date().getMonth() + 1;
|
||||
@@ -105,9 +98,6 @@ export async function mainBoot() {
|
||||
}
|
||||
|
||||
if ($i) {
|
||||
// only add post shortcuts if logged in
|
||||
hotkeys['p|n'] = post;
|
||||
|
||||
defaultStore.loaded.then(() => {
|
||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
||||
@@ -124,7 +114,7 @@ export async function mainBoot() {
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('announcementCreated', (ev) => {
|
||||
function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
|
||||
const announcement = ev.announcement;
|
||||
if (announcement.display === 'dialog') {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||
@@ -133,7 +123,9 @@ export async function mainBoot() {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('announcementCreated', onAnnouncementCreated);
|
||||
|
||||
if ($i.isDeleted) {
|
||||
alert({
|
||||
@@ -326,6 +318,9 @@ export async function mainBoot() {
|
||||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// 個人宛てお知らせが発行されたとき
|
||||
main.on('announcementCreated', onAnnouncementCreated);
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
@@ -334,7 +329,19 @@ export async function mainBoot() {
|
||||
}
|
||||
|
||||
// shortcut
|
||||
document.addEventListener('keydown', makeHotkey(hotkeys));
|
||||
const keymap = {
|
||||
'p|n': () => {
|
||||
if ($i == null) return;
|
||||
post();
|
||||
},
|
||||
'd': () => {
|
||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||
},
|
||||
's': () => {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
|
||||
|
||||
initializeSw();
|
||||
}
|
||||
|
@@ -5,9 +5,12 @@
|
||||
|
||||
import { createApp, defineAsyncComponent } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
|
||||
export async function subBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
defineAsyncComponent(() => import('@/ui/minimum.vue')),
|
||||
));
|
||||
|
||||
emojiPicker.init();
|
||||
}
|
||||
|
@@ -153,7 +153,7 @@ onMounted(() => {
|
||||
background: linear-gradient(0deg, #ffee20, #eb7018);
|
||||
}
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -173,7 +173,7 @@ onMounted(() => {
|
||||
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
||||
}
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@@ -250,7 +250,6 @@ function onMousedown(evt: MouseEvent): void {
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: solid 2px var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
@@ -87,17 +87,7 @@ async function onClick() {
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div style="position: relative;">
|
||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
|
||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt">
|
||||
<div class="banner" :style="bannerStyle">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||
@@ -80,6 +80,7 @@ const bannerStyle = computed(() => {
|
||||
<style lang="scss" scoped>
|
||||
.eftoefju {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
@@ -87,6 +88,22 @@ const bannerStyle = computed(() => {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 0 2px var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
> .banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
@@ -40,6 +40,14 @@ const remaining = computed(() => {
|
||||
.link {
|
||||
display: block;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
.root {
|
||||
box-shadow: inset 0 0 0 2px var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
|
@@ -30,7 +30,7 @@ import * as os from '@/os.js';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
|
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
>
|
||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
||||
<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
@@ -45,11 +45,11 @@ function toggle() {
|
||||
.label {
|
||||
margin-left: 4px;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')" @esc="cancel()">
|
||||
<div :class="$style.root">
|
||||
<div v-if="icon" :class="$style.icon">
|
||||
<i :class="icon"></i>
|
||||
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
||||
import { ref, shallowRef, computed } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
@@ -156,10 +156,6 @@ function onBgClick() {
|
||||
if (props.cancelableByBgClick) cancel();
|
||||
}
|
||||
*/
|
||||
function onKeydown(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Escape') cancel();
|
||||
}
|
||||
|
||||
function onInputKeydown(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
|
||||
evt.preventDefault();
|
||||
@@ -167,14 +163,6 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||
ok();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -115,14 +115,14 @@ function onDragend() {
|
||||
background: rgba(#000, 0.05);
|
||||
|
||||
> .label {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #0b65a5;
|
||||
}
|
||||
|
||||
&.red {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #c12113;
|
||||
}
|
||||
}
|
||||
@@ -133,14 +133,14 @@ function onDragend() {
|
||||
background: rgba(#000, 0.1);
|
||||
|
||||
> .label {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #0b588c;
|
||||
}
|
||||
|
||||
&.red {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #ce2212;
|
||||
}
|
||||
}
|
||||
@@ -159,8 +159,8 @@ function onDragend() {
|
||||
}
|
||||
|
||||
> .label {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -181,8 +181,8 @@ function onDragend() {
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -190,14 +190,14 @@ function onDragend() {
|
||||
background: #0c7ac9;
|
||||
}
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 57px;
|
||||
width: 28px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
top: 57px;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
@@ -205,8 +205,8 @@ function onDragend() {
|
||||
}
|
||||
|
||||
&.red {
|
||||
&:before,
|
||||
&:after {
|
||||
&::before,
|
||||
&::after {
|
||||
background: #c12113;
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -296,7 +296,7 @@ function onContextmenu(ev: MouseEvent) {
|
||||
cursor: pointer;
|
||||
|
||||
&.draghover {
|
||||
&:after {
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
@@ -5,7 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
|
||||
<input
|
||||
ref="searchEl"
|
||||
:value="q"
|
||||
class="search"
|
||||
data-prevent-emoji-insert
|
||||
:class="{ filled: q != null && q != '' }"
|
||||
:placeholder="i18n.ts.search"
|
||||
type="search"
|
||||
autocapitalize="off"
|
||||
@input="input()"
|
||||
@paste.stop="paste"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
||||
<div ref="emojisEl" class="emojis" tabindex="-1">
|
||||
<section class="result">
|
||||
@@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'chosen', v: string): void;
|
||||
(ev: 'esc'): void;
|
||||
}>();
|
||||
|
||||
const searchEl = shallowRef<HTMLInputElement>();
|
||||
@@ -433,9 +446,18 @@ function paste(event: ClipboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function onEnter(ev: KeyboardEvent) {
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||
done();
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
done();
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
emit('esc');
|
||||
}
|
||||
}
|
||||
|
||||
function done(query?: string): boolean | void {
|
||||
@@ -702,11 +724,6 @@ defineExpose({
|
||||
border-radius: 4px;
|
||||
font-size: 24px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: solid 2px var(--focus);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
@@ -9,10 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
v-slot="{ type, maxHeight }"
|
||||
:zPriority="'middle'"
|
||||
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:hasInteractionWithOtherFocusTrappedEls="true"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
:src="src"
|
||||
@click="modal?.close()"
|
||||
@esc="modal?.close()"
|
||||
@opening="opening"
|
||||
@close="emit('close')"
|
||||
@closed="emit('closed')"
|
||||
@@ -28,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@chosen="chosen"
|
||||
@esc="modal?.close()"
|
||||
/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel">
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||
@@ -39,6 +39,10 @@ const props = defineProps<{
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 16px;
|
||||
|
||||
|
@@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||
<button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||
<div :class="$style.headerText">
|
||||
<div>
|
||||
<div :class="$style.headerTextMain">
|
||||
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||
</div>
|
||||
<div :class="$style.headerTextSub">
|
||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||
<i v-else class="ti ti-chevron-down icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
||||
@@ -147,6 +147,10 @@ onMounted(() => {
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
@@ -190,6 +194,12 @@ onMounted(() => {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.headerTextMain,
|
||||
.headerTextSub {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.headerTextSub {
|
||||
color: var(--fgTransparentWeak);
|
||||
font-size: .85em;
|
||||
|
@@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { host } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
@@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
|
||||
const wait = ref(false);
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
if (props.user.isFollowing == null && $i) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
})
|
||||
@@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
|
||||
|
||||
wait.value = true;
|
||||
|
||||
try {
|
||||
@@ -185,17 +189,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@@ -83,7 +83,7 @@ function leaveHover(): void {
|
||||
|
||||
> article {
|
||||
> footer {
|
||||
&:before {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ function leaveHover(): void {
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||
>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) {
|
||||
}
|
||||
|
||||
function drawAvg() {
|
||||
if (!canvas.value || !props.hash) return;
|
||||
if (!canvas.value) return;
|
||||
|
||||
const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888';
|
||||
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// avgColorでお茶をにごす
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
|
||||
}
|
||||
|
||||
async function draw() {
|
||||
if (props.hash == null) return;
|
||||
if (import.meta.env.MODE === 'test' && props.hash == null) return;
|
||||
|
||||
drawAvg();
|
||||
|
||||
if (props.hash == null) return;
|
||||
|
||||
if (props.onlyAvgColor) return;
|
||||
|
||||
const work = await canvasPromise;
|
||||
|
@@ -62,7 +62,7 @@ import { computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
|
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||
<div class="main">
|
||||
<template v-for="item in items" :key="item.text">
|
||||
|
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@contextmenu.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<button v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<button v-if="hide" :class="$style.hidden" @click="show">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
|
||||
@@ -39,23 +39,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<audio
|
||||
ref="audioEl"
|
||||
preload="metadata"
|
||||
@keydown.prevent="() => {}"
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
|
||||
<button
|
||||
:class="['_button', $style.controlButton]"
|
||||
tabindex="-1"
|
||||
@click.stop="togglePlayPause"
|
||||
>
|
||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||
<i v-else class="ti ti-player-play-filled"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||
<button class="_button" :class="$style.controlButton" @click="showMenu">
|
||||
<button
|
||||
:class="['_button', $style.controlButton]"
|
||||
tabindex="-1"
|
||||
@click.stop="() => {}"
|
||||
@mousedown.prevent.stop="showMenu"
|
||||
>
|
||||
<i class="ti ti-settings"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
||||
<button class="_button" :class="$style.controlButton" @click="toggleMute">
|
||||
<button
|
||||
:class="['_button', $style.controlButton]"
|
||||
tabindex="-1"
|
||||
@click.stop="toggleMute"
|
||||
>
|
||||
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
||||
<i v-else class="ti ti-volume"></i>
|
||||
</button>
|
||||
@@ -80,6 +94,7 @@ import type { MenuItem } from '@/types/menu.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { hms } from '@/filters/hms.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
@@ -90,32 +105,44 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const keymap = {
|
||||
'up': () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
volume.value = Math.min(volume.value + 0.1, 1);
|
||||
}
|
||||
'up': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'down': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'left': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'right': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
if (hasFocus() && audioEl.value) {
|
||||
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
|
||||
}
|
||||
},
|
||||
},
|
||||
'space': () => {
|
||||
if (hasFocus()) {
|
||||
togglePlayPause();
|
||||
}
|
||||
},
|
||||
};
|
||||
} as const satisfies Keymap;
|
||||
|
||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||
function hasFocus() {
|
||||
@@ -129,6 +156,18 @@ const audioEl = shallowRef<HTMLAudioElement>();
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||
|
||||
async function show() {
|
||||
if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts.sensitiveMediaRevealConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
hide.value = false;
|
||||
}
|
||||
|
||||
// Menu
|
||||
const menuShowing = ref(false);
|
||||
|
||||
@@ -358,7 +397,7 @@ onDeactivated(() => {
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -424,6 +463,10 @@ onDeactivated(() => {
|
||||
color: var(--accent);
|
||||
background-color: var(--accentedBg);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
|
||||
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
|
||||
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
|
||||
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
|
||||
<b>{{ i18n.ts.sensitive }}</b>
|
||||
<span>{{ i18n.ts.clickToShow }}</span>
|
||||
@@ -24,24 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, watch, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkMediaAudio from '@/components/MkMediaAudio.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
media: Misskey.entities.DriveFile;
|
||||
}>(), {
|
||||
});
|
||||
}>();
|
||||
|
||||
const audioEl = shallowRef<HTMLAudioElement>();
|
||||
const hide = ref(true);
|
||||
|
||||
watch(audioEl, () => {
|
||||
if (audioEl.value) {
|
||||
audioEl.value.volume = 0.3;
|
||||
async function show() {
|
||||
if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts.sensitiveMediaRevealConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
});
|
||||
|
||||
hide.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -83,11 +83,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||
: props.image.thumbnailUrl,
|
||||
);
|
||||
|
||||
function onclick() {
|
||||
async function onclick(ev: MouseEvent) {
|
||||
if (!props.controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hide.value) {
|
||||
ev.stopPropagation();
|
||||
if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts.sensitiveMediaRevealConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
hide.value = false;
|
||||
}
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ import XVideo from '@/components/MkMediaVideo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
|
||||
const props = defineProps<{
|
||||
mediaList: Misskey.entities.DriveFile[];
|
||||
@@ -49,7 +50,9 @@ const gallery = shallowRef<HTMLDivElement>();
|
||||
const pswpZIndex = os.claimZIndex('middle');
|
||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
let lightbox: PhotoSwipeLightbox | null;
|
||||
let lightbox: PhotoSwipeLightbox | null = null;
|
||||
|
||||
let activeEl: HTMLElement | null = null;
|
||||
|
||||
const popstateHandler = (): void => {
|
||||
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
||||
@@ -60,7 +63,7 @@ const popstateHandler = (): void => {
|
||||
async function calcAspectRatio() {
|
||||
if (!gallery.value) return;
|
||||
|
||||
let img = props.mediaList[0];
|
||||
const img = props.mediaList[0];
|
||||
|
||||
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||
gallery.value.style.aspectRatio = '';
|
||||
@@ -131,18 +134,17 @@ onMounted(() => {
|
||||
bgOpacity: 1,
|
||||
showAnimationDuration: 100,
|
||||
hideAnimationDuration: 100,
|
||||
returnFocus: false,
|
||||
pswpModule: PhotoSwipe,
|
||||
});
|
||||
|
||||
lightbox.on('itemData', (ev) => {
|
||||
const { itemData } = ev;
|
||||
|
||||
lightbox.addFilter('itemData', (itemData) => {
|
||||
// element is children
|
||||
const { element } = itemData;
|
||||
|
||||
const id = element?.dataset.id;
|
||||
const file = props.mediaList.find(media => media.id === id);
|
||||
if (!file) return;
|
||||
if (!file) return itemData;
|
||||
|
||||
itemData.src = file.url;
|
||||
itemData.w = Number(file.properties.width);
|
||||
@@ -154,44 +156,54 @@ onMounted(() => {
|
||||
itemData.alt = file.comment ?? file.name;
|
||||
itemData.comment = file.comment ?? file.name;
|
||||
itemData.thumbCropped = true;
|
||||
|
||||
return itemData;
|
||||
});
|
||||
|
||||
lightbox.on('uiRegister', () => {
|
||||
lightbox?.pswp?.ui?.registerElement({
|
||||
name: 'altText',
|
||||
className: 'pwsp__alt-text-container',
|
||||
className: 'pswp__alt-text-container',
|
||||
appendTo: 'wrapper',
|
||||
onInit: (el, pwsp) => {
|
||||
let textBox = document.createElement('p');
|
||||
textBox.className = 'pwsp__alt-text _acrylic';
|
||||
onInit: (el, pswp) => {
|
||||
const textBox = document.createElement('p');
|
||||
textBox.className = 'pswp__alt-text _acrylic';
|
||||
el.appendChild(textBox);
|
||||
|
||||
pwsp.on('change', () => {
|
||||
textBox.textContent = pwsp.currSlide?.data.comment;
|
||||
pswp.on('change', () => {
|
||||
textBox.textContent = pswp.currSlide?.data.comment;
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
lightbox.init();
|
||||
|
||||
window.addEventListener('popstate', popstateHandler);
|
||||
|
||||
lightbox.on('beforeOpen', () => {
|
||||
lightbox.on('afterInit', () => {
|
||||
activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
focusParent(activeEl, true, true);
|
||||
lightbox?.pswp?.element?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
history.pushState(null, '', '#pswp');
|
||||
});
|
||||
|
||||
lightbox.on('close', () => {
|
||||
lightbox.on('destroy', () => {
|
||||
focusParent(activeEl, true, false);
|
||||
activeEl = null;
|
||||
if (window.location.hash === '#pswp') {
|
||||
history.back();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', popstateHandler);
|
||||
|
||||
lightbox.init();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('popstate', popstateHandler);
|
||||
lightbox?.destroy();
|
||||
lightbox = null;
|
||||
activeEl = null;
|
||||
});
|
||||
|
||||
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||
@@ -199,6 +211,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
||||
};
|
||||
|
||||
const openGallery = () => {
|
||||
if (props.mediaList.filter(media => previewable(media)).length > 0) {
|
||||
lightbox?.loadAndOpen(0);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openGallery,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -298,7 +320,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||
backdrop-filter: var(--modalBgFilter);
|
||||
}
|
||||
|
||||
.pwsp__alt-text-container {
|
||||
.pswp__alt-text-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -312,7 +334,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.pwsp__alt-text {
|
||||
.pswp__alt-text {
|
||||
color: var(--fg);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@contextmenu.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<button v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<button v-if="hide" :class="$style.hidden" @click="show">
|
||||
<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-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||
@@ -112,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { hms } from '@/filters/hms.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
@@ -127,32 +128,44 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const keymap = {
|
||||
'up': () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
volume.value = Math.min(volume.value + 0.1, 1);
|
||||
}
|
||||
'up': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'down': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'left': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
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);
|
||||
}
|
||||
'right': {
|
||||
allowRepeat: true,
|
||||
callback: () => {
|
||||
if (hasFocus() && videoEl.value) {
|
||||
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
|
||||
}
|
||||
},
|
||||
},
|
||||
'space': () => {
|
||||
if (hasFocus()) {
|
||||
togglePlayPause();
|
||||
}
|
||||
},
|
||||
};
|
||||
} as const satisfies Keymap;
|
||||
|
||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||
function hasFocus() {
|
||||
@@ -163,6 +176,18 @@ function hasFocus() {
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||
|
||||
async function show() {
|
||||
if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts.sensitiveMediaRevealConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
hide.value = false;
|
||||
}
|
||||
|
||||
// Menu
|
||||
const menuShowing = ref(false);
|
||||
|
||||
@@ -468,7 +493,7 @@ onDeactivated(() => {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -575,6 +600,10 @@ onDeactivated(() => {
|
||||
border-radius: 99rem;
|
||||
|
||||
font-size: 1.1rem;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.videoLoading {
|
||||
@@ -638,6 +667,10 @@ onDeactivated(() => {
|
||||
&:hover {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
@@ -19,7 +19,6 @@ const props = defineProps<{
|
||||
targetElement: HTMLElement;
|
||||
rootElement: HTMLElement;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -27,6 +26,8 @@ const emit = defineEmits<{
|
||||
(ev: 'actioned'): void;
|
||||
}>();
|
||||
|
||||
provide('isNestingMenu', true);
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const align = 'left';
|
||||
|
||||
|
@@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div role="menu">
|
||||
<div role="menu" @focusin.passive.stop="() => {}">
|
||||
<div
|
||||
ref="itemsEl" v-hotkey="keymap"
|
||||
ref="itemsEl"
|
||||
v-hotkey="keymap"
|
||||
tabindex="0"
|
||||
class="_popup _shadow"
|
||||
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
:class="{
|
||||
[$style.root]: true,
|
||||
[$style.center]: align === 'center',
|
||||
[$style.asDrawer]: asDrawer,
|
||||
}"
|
||||
:style="{
|
||||
width: (width && !asDrawer) ? `${width}px` : '',
|
||||
maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
|
||||
}"
|
||||
@keydown.stop="() => {}"
|
||||
@contextmenu.self.prevent="() => {}"
|
||||
>
|
||||
<template v-for="(item, i) in (items2 ?? [])">
|
||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
||||
<template v-for="item in (items2 ?? [])">
|
||||
<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
|
||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||
</span>
|
||||
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
|
||||
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
||||
<span><MkEllipsis/></span>
|
||||
</span>
|
||||
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkA
|
||||
v-else-if="item.type === 'link'"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item]"
|
||||
:to="item.to"
|
||||
@click.passive="close(true)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
@@ -28,20 +47,49 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</MkA>
|
||||
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<a
|
||||
v-else-if="item.type === 'a'"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item]"
|
||||
:href="item.href"
|
||||
:target="item.target"
|
||||
:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
|
||||
:download="item.download"
|
||||
@click.passive="close(true)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<button
|
||||
v-else-if="item.type === 'user'"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, { [$style.active]: item.active }]"
|
||||
@click.prevent="item.active ? close(false) : clicked(item.action, $event)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||
<div v-if="item.indicate" :class="$style.item_content">
|
||||
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</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)">
|
||||
<button
|
||||
v-else-if="item.type === 'switch'"
|
||||
role="menuitemcheckbox"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item]"
|
||||
:disabled="unref(item.disabled)"
|
||||
@click.prevent="switchItem(item)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<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">
|
||||
@@ -49,29 +97,61 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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)">
|
||||
<button
|
||||
v-else-if="item.type === 'radio'"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||
:disabled="unref(item.disabled)"
|
||||
@mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||
@keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||
@click.prevent="!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)">
|
||||
<button
|
||||
v-else-if="item.type === 'radioOption'"
|
||||
role="menuitemradio"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]"
|
||||
@click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<div :class="$style.icon">
|
||||
<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
|
||||
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(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)">
|
||||
<button
|
||||
v-else-if="item.type === 'parent'"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||
@mouseenter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||
@keydown.enter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||
@click.prevent="!preferClick ? null : showChildren(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 :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<button
|
||||
v-else role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
@@ -80,24 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
|
||||
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
||||
<span>{{ i18n.ts.none }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="childMenu">
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
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';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { isFocusable } from '@/scripts/focus.js';
|
||||
import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
|
||||
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
||||
</script>
|
||||
@@ -107,7 +189,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
viaKeyboard?: boolean;
|
||||
asDrawer?: boolean;
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
@@ -119,17 +200,28 @@ const emit = defineEmits<{
|
||||
(ev: 'hide'): void;
|
||||
}>();
|
||||
|
||||
const itemsEl = shallowRef<HTMLDivElement>();
|
||||
const isNestingMenu = inject<boolean>('isNestingMenu', false);
|
||||
|
||||
const itemsEl = shallowRef<HTMLElement>();
|
||||
|
||||
const items2 = ref<InnerMenuItem[]>();
|
||||
|
||||
const child = shallowRef<InstanceType<typeof XChild>>();
|
||||
|
||||
const keymap = computed(() => ({
|
||||
'up|k|shift+tab': focusUp,
|
||||
'down|j|tab': focusDown,
|
||||
'esc': close,
|
||||
}));
|
||||
const keymap = {
|
||||
'up|k|shift+tab': {
|
||||
allowRepeat: true,
|
||||
callback: () => focusUp(),
|
||||
},
|
||||
'down|j|tab': {
|
||||
allowRepeat: true,
|
||||
callback: () => focusDown(),
|
||||
},
|
||||
'esc': {
|
||||
allowRepeat: true,
|
||||
callback: () => close(false),
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
|
||||
const childShowingItem = ref<MenuItem | null>();
|
||||
|
||||
@@ -167,25 +259,19 @@ function childActioned() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
const onGlobalMousedown = (event: MouseEvent) => {
|
||||
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
|
||||
if (child.value && child.value.checkHit(event)) return;
|
||||
closeChild();
|
||||
};
|
||||
|
||||
let childCloseTimer: null | number = null;
|
||||
|
||||
function onItemMouseEnter(item) {
|
||||
function onItemMouseEnter() {
|
||||
childCloseTimer = window.setTimeout(() => {
|
||||
closeChild();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onItemMouseLeave(item) {
|
||||
function onItemMouseLeave() {
|
||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||
}
|
||||
|
||||
async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||
async function showRadioOptions(item: MenuRadio, ev: Event) {
|
||||
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
||||
const value = item.options[key];
|
||||
return {
|
||||
@@ -200,7 +286,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||
|
||||
if (props.asDrawer) {
|
||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||
emit('close');
|
||||
close(false);
|
||||
});
|
||||
emit('hide');
|
||||
} else {
|
||||
@@ -210,7 +296,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||
async function showChildren(item: MenuParent, ev: Event) {
|
||||
const children: MenuItem[] = await (async () => {
|
||||
if (childrenCache.has(item)) {
|
||||
return childrenCache.get(item)!;
|
||||
@@ -227,7 +313,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||
|
||||
if (props.asDrawer) {
|
||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||
emit('close');
|
||||
close(false);
|
||||
});
|
||||
emit('hide');
|
||||
} else {
|
||||
@@ -246,15 +332,11 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
|
||||
}
|
||||
|
||||
function close(actioned = false) {
|
||||
emit('close', actioned);
|
||||
}
|
||||
|
||||
function focusUp() {
|
||||
focusPrev(document.activeElement);
|
||||
}
|
||||
|
||||
function focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
disposeHandlers();
|
||||
nextTick(() => {
|
||||
closeChild();
|
||||
emit('close', actioned);
|
||||
});
|
||||
}
|
||||
|
||||
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||
@@ -262,25 +344,75 @@ function switchItem(item: MenuSwitch & { ref: any }) {
|
||||
item.ref = !item.ref;
|
||||
}
|
||||
|
||||
function getValue<T>(item?: ComputedRef<T> | T) {
|
||||
return isRef(item) ? item.value : item;
|
||||
function focusUp() {
|
||||
if (disposed) return;
|
||||
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||
|
||||
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||
const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
|
||||
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||
|
||||
targetElement.focus();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.viaKeyboard) {
|
||||
nextTick(() => {
|
||||
if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
|
||||
});
|
||||
function focusDown() {
|
||||
if (disposed) return;
|
||||
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||
|
||||
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||
const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
|
||||
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||
|
||||
targetElement.focus();
|
||||
}
|
||||
|
||||
const onGlobalFocusin = (ev: FocusEvent) => {
|
||||
if (disposed) return;
|
||||
if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return;
|
||||
nextTick(() => {
|
||||
if (itemsEl.value != null && isFocusable(itemsEl.value)) {
|
||||
itemsEl.value.focus({ preventScroll: true });
|
||||
nextTick(() => focusDown());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onGlobalMousedown = (ev: MouseEvent) => {
|
||||
if (disposed) return;
|
||||
if (childTarget.value?.contains(getNodeOrNull(ev.target))) return;
|
||||
if (child.value?.checkHit(ev)) return;
|
||||
closeChild();
|
||||
};
|
||||
|
||||
const setupHandlers = () => {
|
||||
if (!isNestingMenu) {
|
||||
document.addEventListener('focusin', onGlobalFocusin, { passive: true });
|
||||
}
|
||||
|
||||
// TODO: アクティブな要素までスクロール
|
||||
//itemsEl.scrollTo();
|
||||
|
||||
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
||||
};
|
||||
|
||||
let disposed = false;
|
||||
|
||||
const disposeHandlers = () => {
|
||||
disposed = true;
|
||||
if (!isNestingMenu) {
|
||||
document.removeEventListener('focusin', onGlobalFocusin);
|
||||
}
|
||||
document.removeEventListener('mousedown', onGlobalMousedown);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setupHandlers();
|
||||
|
||||
if (!isNestingMenu) {
|
||||
nextTick(() => itemsEl.value?.focus({ preventScroll: true }));
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousedown', onGlobalMousedown);
|
||||
disposeHandlers();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -293,6 +425,10 @@ onBeforeUnmount(() => {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.center {
|
||||
> .item {
|
||||
text-align: center;
|
||||
@@ -310,7 +446,7 @@ onBeforeUnmount(() => {
|
||||
font-size: 1em;
|
||||
padding: 12px 24px;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
width: calc(100% - 24px);
|
||||
border-radius: 12px;
|
||||
}
|
||||
@@ -340,8 +476,10 @@ onBeforeUnmount(() => {
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none !important;
|
||||
color: var(--menuFg, var(--fg));
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -355,56 +493,56 @@ onBeforeUnmount(() => {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
&:not(:hover):not(:active)::before {
|
||||
outline: var(--focus) solid 2px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover,
|
||||
&:focus-visible:active,
|
||||
&:focus-visible.active {
|
||||
color: var(--menuHoverFg, var(--accent));
|
||||
|
||||
&::before {
|
||||
background-color: var(--menuHoverBg, var(--accentedBg));
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:focus-visible):active,
|
||||
&:not(:focus-visible).active {
|
||||
color: var(--menuActiveFg, var(--fgOnAccent));
|
||||
|
||||
&::before {
|
||||
background-color: var(--menuActiveBg, var(--accent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff2a2a;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
&:before {
|
||||
background: #ff4242;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #fff;
|
||||
|
||||
&:before {
|
||||
background: #d42e2e !important;
|
||||
}
|
||||
}
|
||||
--menuFg: #ff2a2a;
|
||||
--menuHoverFg: #fff;
|
||||
--menuHoverBg: #ff4242;
|
||||
--menuActiveFg: #fff;
|
||||
--menuActiveBg: #d42e2e;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--fgOnAccent) !important;
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
&.radio {
|
||||
--menuActiveFg: var(--accent);
|
||||
--menuActiveBg: var(--accentedBg);
|
||||
}
|
||||
|
||||
&.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;
|
||||
&.parent {
|
||||
--menuActiveFg: var(--accent);
|
||||
--menuActiveBg: var(--accentedBg);
|
||||
}
|
||||
|
||||
&.label {
|
||||
@@ -422,22 +560,6 @@ onBeforeUnmount(() => {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.parent {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
&.childShowing {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item_content {
|
||||
@@ -456,18 +578,6 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switchDisabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switchButton {
|
||||
margin-left: -2px;
|
||||
--height: 1.35em;
|
||||
@@ -479,14 +589,6 @@ onBeforeUnmount(() => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.switchInput {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
line-height: 1;
|
||||
@@ -515,12 +617,12 @@ onBeforeUnmount(() => {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
.radio {
|
||||
.radioIcon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -.125em;
|
||||
vertical-align: -0.125em;
|
||||
border-radius: 50%;
|
||||
border: solid 2px var(--divider);
|
||||
background-color: var(--panel);
|
||||
|
@@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
||||
[$style.transition_send_leaveTo]: transitionName === 'send',
|
||||
})"
|
||||
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
|
||||
:duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened"
|
||||
>
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
|
||||
<slot :max-height="maxHeight" :type="type"></slot>
|
||||
@@ -47,6 +47,9 @@ import * as os from '@/os.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusTrap } from '@/scripts/focus-trap.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
|
||||
function getFixedContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
@@ -68,6 +71,8 @@ const props = withDefaults(defineProps<{
|
||||
zPriority?: 'low' | 'middle' | 'high';
|
||||
noOverlap?: boolean;
|
||||
transparentBg?: boolean;
|
||||
hasInteractionWithOtherFocusTrappedEls?: boolean;
|
||||
returnFocusTo?: HTMLElement | null;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
src: null,
|
||||
@@ -76,6 +81,8 @@ const props = withDefaults(defineProps<{
|
||||
zPriority: 'low',
|
||||
noOverlap: true,
|
||||
transparentBg: false,
|
||||
hasInteractionWithOtherFocusTrappedEls: false,
|
||||
returnFocusTo: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -93,6 +100,7 @@ const maxHeight = ref<number>();
|
||||
const fixed = ref(false);
|
||||
const transformOrigin = ref('center');
|
||||
const showing = ref(true);
|
||||
const modalRootEl = shallowRef<HTMLElement>();
|
||||
const content = shallowRef<HTMLElement>();
|
||||
const zIndex = os.claimZIndex(props.zPriority);
|
||||
const useSendAnime = ref(false);
|
||||
@@ -131,6 +139,7 @@ const transitionDuration = computed((() =>
|
||||
: 0
|
||||
));
|
||||
|
||||
let releaseFocusTrap: (() => void) | null = null;
|
||||
let contentClicking = false;
|
||||
|
||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||
@@ -154,8 +163,11 @@ if (type.value === 'drawer') {
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
'esc': () => emit('esc'),
|
||||
};
|
||||
'esc': {
|
||||
allowRepeat: true,
|
||||
callback: () => emit('esc'),
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
|
||||
const MARGIN = 16;
|
||||
const SCROLLBAR_THICKNESS = 16;
|
||||
@@ -292,6 +304,10 @@ const onOpened = () => {
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
const onClosed = () => {
|
||||
emit('closed');
|
||||
};
|
||||
|
||||
const alignObserver = new ResizeObserver((entries, observer) => {
|
||||
align();
|
||||
});
|
||||
@@ -309,6 +325,20 @@ onMounted(() => {
|
||||
align();
|
||||
}, { immediate: true });
|
||||
|
||||
watch([showing, () => props.manualShowing], ([showing, manualShowing]) => {
|
||||
if (manualShowing === true || (manualShowing == null && showing === true)) {
|
||||
if (modalRootEl.value != null) {
|
||||
const { release } = focusTrap(modalRootEl.value, props.hasInteractionWithOtherFocusTrappedEls);
|
||||
|
||||
releaseFocusTrap = release;
|
||||
modalRootEl.value.focus();
|
||||
}
|
||||
} else {
|
||||
releaseFocusTrap?.();
|
||||
focusParent(props.returnFocusTo ?? props.src, true, false);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
nextTick(() => {
|
||||
alignObserver.observe(content.value!);
|
||||
});
|
||||
|
@@ -4,15 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
|
||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
|
||||
<div ref="headerEl" :class="$style.header">
|
||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
|
||||
<span :class="$style.title">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
|
||||
<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
|
||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||
@@ -27,11 +27,13 @@ import MkModal from './MkModal.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
withOkButton: boolean;
|
||||
withCloseButton: boolean;
|
||||
okButtonDisabled: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}>(), {
|
||||
withOkButton: false,
|
||||
withCloseButton: true,
|
||||
okButtonDisabled: false,
|
||||
width: 400,
|
||||
height: 500,
|
||||
@@ -42,6 +44,7 @@ const emit = defineEmits<{
|
||||
(event: 'close'): void;
|
||||
(event: 'closed'): void;
|
||||
(event: 'ok'): void;
|
||||
(event: 'esc'): void;
|
||||
}>();
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
@@ -50,21 +53,13 @@ const headerEl = shallowRef<HTMLElement>();
|
||||
const bodyWidth = ref(0);
|
||||
const bodyHeight = ref(0);
|
||||
|
||||
const close = () => {
|
||||
function close() {
|
||||
modal.value?.close();
|
||||
};
|
||||
}
|
||||
|
||||
const onBgClick = () => {
|
||||
function onBgClick() {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onKeydown = (evt) => {
|
||||
if (evt.which === 27) { // Esc
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (rootEl.value == null || headerEl.value == null) return;
|
||||
|
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||
:tabindex="!isDeleted ? '-1' : undefined"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</I18n>
|
||||
<div :class="$style.renoteInfo">
|
||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()">
|
||||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="renoteButton"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
@mousedown="renote()"
|
||||
@mousedown.prevent="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
@@ -125,10 +125,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</button>
|
||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -174,8 +174,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
@@ -197,7 +196,10 @@ import { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { host } from '@/config.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
@@ -256,6 +258,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
@@ -276,6 +279,11 @@ const renoteCollapsed = ref(
|
||||
),
|
||||
);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
}));
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
@@ -294,15 +302,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
'e|a|plus': () => react(true),
|
||||
'q': () => renote(true),
|
||||
'up|k|shift+tab': focusBefore,
|
||||
'down|j|tab': focusAfter,
|
||||
'esc': blur,
|
||||
'm|o': () => showMenu(true),
|
||||
's': () => showContent.value !== showContent.value,
|
||||
};
|
||||
'r': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
reply();
|
||||
},
|
||||
'e|a|plus': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
react();
|
||||
},
|
||||
'q': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
renote();
|
||||
},
|
||||
'm': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
showMenu();
|
||||
},
|
||||
'c': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
||||
clip();
|
||||
},
|
||||
'o': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
galleryEl.value?.openGallery();
|
||||
},
|
||||
'v|enter': () => {
|
||||
if (renoteCollapsed.value) {
|
||||
renoteCollapsed.value = false;
|
||||
} else if (appearNote.value.cw != null) {
|
||||
showContent.value = !showContent.value;
|
||||
} else if (isLong) {
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
},
|
||||
'esc': {
|
||||
allowRepeat: true,
|
||||
callback: () => blur(),
|
||||
},
|
||||
'up|k|shift+tab': {
|
||||
allowRepeat: true,
|
||||
callback: () => focusBefore(),
|
||||
},
|
||||
'down|j|tab': {
|
||||
allowRepeat: true,
|
||||
callback: () => focusAfter(),
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
|
||||
provide('react', (reaction: string) => {
|
||||
misskeyApi('notes/reactions/create', {
|
||||
@@ -371,7 +417,7 @@ if (!props.mock) {
|
||||
}
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
|
||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
||||
@@ -380,22 +426,21 @@ function renote(viaKeyboard = false) {
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
function reply(): void {
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
os.post({
|
||||
reply: appearNote.value,
|
||||
channel: appearNote.value.channel,
|
||||
animation: !viaKeyboard,
|
||||
}).then(() => {
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
function react(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
function react(): void {
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.playMisskeySfx('reaction');
|
||||
@@ -489,18 +534,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu(viaKeyboard = false): void {
|
||||
function showMenu(): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus).finally(cleanup);
|
||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip() {
|
||||
async function clip(): Promise<void> {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
@@ -508,7 +551,7 @@ async function clip() {
|
||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
function showRenoteMenu(): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
@@ -528,23 +571,19 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
}
|
||||
|
||||
if (isMyRenote) {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
getUnrenote(),
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
], renoteTime.value);
|
||||
} else {
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
], renoteTime.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,11 +596,11 @@ function blur() {
|
||||
}
|
||||
|
||||
function focusBefore() {
|
||||
focusPrev(rootEl.value ?? null);
|
||||
focusPrev(rootEl.value);
|
||||
}
|
||||
|
||||
function focusAfter() {
|
||||
focusNext(rootEl.value ?? null);
|
||||
focusNext(rootEl.value);
|
||||
}
|
||||
|
||||
function readPromo() {
|
||||
@@ -599,7 +638,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
@@ -612,7 +651,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
margin: auto;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
border: dashed 1px var(--focus);
|
||||
border: dashed 2px var(--focus);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
:class="$style.root"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
>
|
||||
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
||||
<div v-if="!conversationLoaded" style="padding: 16px">
|
||||
@@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</I18n>
|
||||
</span>
|
||||
<div :class="$style.renoteInfo">
|
||||
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
|
||||
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()">
|
||||
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
@@ -92,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
@@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="renoteButton"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
@mousedown="renote()"
|
||||
@mousedown.prevent="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
@@ -133,10 +134,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
|
||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -208,7 +209,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
@@ -221,6 +222,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
@@ -233,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
@@ -280,6 +283,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
@@ -293,14 +297,31 @@ const conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
}));
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
'e|a|plus': () => react(true),
|
||||
'q': () => renote(true),
|
||||
'esc': blur,
|
||||
'm|o': () => showMenu(true),
|
||||
's': () => showContent.value !== showContent.value,
|
||||
};
|
||||
'r': () => reply(),
|
||||
'e|a|plus': () => react(),
|
||||
'q': () => renote(),
|
||||
'm': () => showMenu(),
|
||||
'c': () => {
|
||||
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
||||
clip();
|
||||
},
|
||||
'o': () => galleryEl.value?.openGallery(),
|
||||
'v|enter': () => {
|
||||
if (appearNote.value.cw != null) {
|
||||
showContent.value = !showContent.value;
|
||||
}
|
||||
},
|
||||
'esc': {
|
||||
allowRepeat: true,
|
||||
callback: () => blur(),
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
|
||||
provide('react', (reaction: string) => {
|
||||
misskeyApi('notes/reactions/create', {
|
||||
@@ -380,30 +401,27 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
});
|
||||
}
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
function renote() {
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
|
||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
||||
os.popupMenu(menu, renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
os.popupMenu(menu, renoteButton.value);
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
function reply(): void {
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
os.post({
|
||||
reply: appearNote.value,
|
||||
channel: appearNote.value.channel,
|
||||
animation: !viaKeyboard,
|
||||
}).then(() => {
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
function react(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
function react(): void {
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.playMisskeySfx('reaction');
|
||||
@@ -412,7 +430,7 @@ function react(viaKeyboard = false): void {
|
||||
noteId: appearNote.value.id,
|
||||
reaction: '❤️',
|
||||
});
|
||||
const el = reactButton.value as HTMLElement | null | undefined;
|
||||
const el = reactButton.value;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
@@ -476,20 +494,18 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu(viaKeyboard = false): void {
|
||||
function showMenu(): void {
|
||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus).finally(cleanup);
|
||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip() {
|
||||
async function clip(): Promise<void> {
|
||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
function showRenoteMenu(): void {
|
||||
if (!isMyRenote) return;
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
@@ -500,9 +516,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
});
|
||||
isDeleted.value = true;
|
||||
},
|
||||
}], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
}], renoteTime.value);
|
||||
}
|
||||
|
||||
function focus() {
|
||||
@@ -544,6 +558,28 @@ function loadConversation() {
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
border: dashed 2px var(--focus);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.replyTo {
|
||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<MkAvatar :class="$style.avatar" :user="user" link preview/>
|
||||
<MkAvatar :class="$style.avatar" :user="user"/>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<MkUserName :user="user" :nowrap="true"/>
|
||||
|
@@ -343,7 +343,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
position: absolute;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj">
|
||||
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
||||
<MediaImage
|
||||
:image="page.eyeCatchingImage"
|
||||
@@ -50,12 +50,29 @@ const props = defineProps<{
|
||||
<style lang="scss" scoped>
|
||||
.vhpxefrj {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--radius);
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 0 2px var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
& + article {
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
|
@@ -33,7 +33,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@@ -34,7 +34,9 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
@@ -60,6 +62,11 @@ const timer = computed(() => i18n.tsx._poll[
|
||||
|
||||
const showResult = ref(props.readOnly || isVoted.value);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${props.noteId}`,
|
||||
}));
|
||||
|
||||
// 期限付きアンケート
|
||||
if (props.poll.expiresAt) {
|
||||
const tick = () => {
|
||||
@@ -76,7 +83,7 @@ if (props.poll.expiresAt) {
|
||||
}
|
||||
|
||||
const vote = async (id) => {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
|
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</section>
|
||||
<section v-else-if="expiration === 'after'">
|
||||
<MkInput v-model="after" small type="number" class="input">
|
||||
<MkInput v-model="after" small type="number" min="1" class="input">
|
||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
|
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
@@ -19,8 +19,8 @@ defineProps<{
|
||||
items: MenuItem[];
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
src?: any;
|
||||
returnFocusTo?: HTMLElement | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@@ -367,6 +367,8 @@ function watchForDraft() {
|
||||
watch(files, () => saveDraft(), { deep: true });
|
||||
watch(visibility, () => saveDraft());
|
||||
watch(localOnly, () => saveDraft());
|
||||
watch(quoteId, () => saveDraft());
|
||||
watch(reactionAcceptance, () => saveDraft());
|
||||
}
|
||||
|
||||
function checkMissingMention() {
|
||||
@@ -570,6 +572,7 @@ function clear() {
|
||||
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
||||
|
||||
if (ev.key === 'Escape') emit('esc');
|
||||
}
|
||||
|
||||
@@ -702,6 +705,8 @@ function saveDraft() {
|
||||
files: files.value,
|
||||
poll: poll.value,
|
||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||
quoteId: quoteId.value,
|
||||
reactionAcceptance: reactionAcceptance.value,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -990,6 +995,8 @@ onMounted(() => {
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
});
|
||||
}
|
||||
quoteId.value = draft.data.quoteId;
|
||||
reactionAcceptance.value = draft.data.reactionAcceptance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,9 +1004,11 @@ onMounted(() => {
|
||||
if (props.initialNote) {
|
||||
const init = props.initialNote;
|
||||
text.value = init.text ? init.text : '';
|
||||
files.value = init.files ?? [];
|
||||
cw.value = init.cw ?? null;
|
||||
useCw.value = init.cw != null;
|
||||
cw.value = init.cw ?? null;
|
||||
visibility.value = init.visibility;
|
||||
localOnly.value = init.localOnly ?? false;
|
||||
files.value = init.files ?? [];
|
||||
if (init.poll) {
|
||||
poll.value = {
|
||||
choices: init.poll.choices.map(x => x.text),
|
||||
@@ -1008,9 +1017,13 @@ onMounted(() => {
|
||||
expiredAfter: null,
|
||||
};
|
||||
}
|
||||
visibility.value = init.visibility;
|
||||
localOnly.value = init.localOnly ?? false;
|
||||
if (init.visibleUserIds) {
|
||||
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
});
|
||||
}
|
||||
quoteId.value = init.renote ? init.renote.id : null;
|
||||
reactionAcceptance.value = init.reactionAcceptance;
|
||||
}
|
||||
|
||||
nextTick(() => watchForDraft());
|
||||
@@ -1083,6 +1096,15 @@ defineExpose({
|
||||
margin: 12px 12px 12px 6px;
|
||||
vertical-align: bottom;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
.submitInner {
|
||||
outline: 2px solid var(--fgOnAccent);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()">
|
||||
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
@@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
role="checkbox"
|
||||
@click="toggle"
|
||||
>
|
||||
<input
|
||||
@@ -69,6 +70,11 @@ function toggle(): void {
|
||||
border-color: var(--inputBorderHover) !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background-color: var(--accentedBg) !important;
|
||||
border-color: var(--accentedBg) !important;
|
||||
@@ -78,7 +84,7 @@ function toggle(): void {
|
||||
> .button {
|
||||
border-color: var(--accent);
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
background-color: var(--accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
@@ -104,7 +110,7 @@ function toggle(): void {
|
||||
border-radius: 100%;
|
||||
transition: inherit;
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@@ -81,6 +81,7 @@ function getReactionName(reaction: string): string {
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
padding-top: 4px;
|
||||
white-space: nowrap;
|
||||
|
@@ -6,20 +6,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
||||
<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
|
||||
<div
|
||||
ref="container"
|
||||
tabindex="0"
|
||||
:class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@mousedown.prevent="show"
|
||||
@keydown.space.enter="show"
|
||||
>
|
||||
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
||||
<select
|
||||
ref="inputEl"
|
||||
v-model="v"
|
||||
v-adaptive-border
|
||||
tabindex="-1"
|
||||
:class="$style.inputCore"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@input="onInput"
|
||||
@mousedown.prevent="() => {}"
|
||||
@keydown.prevent="() => {}"
|
||||
>
|
||||
<slot></slot>
|
||||
</select>
|
||||
@@ -75,7 +84,7 @@ const height =
|
||||
props.large ? 39 :
|
||||
36;
|
||||
|
||||
const focus = () => inputEl.value?.focus();
|
||||
const focus = () => container.value?.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
};
|
||||
@@ -126,7 +135,9 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
function show() {
|
||||
focused.value = true;
|
||||
if (opening.value) return;
|
||||
focus();
|
||||
|
||||
opening.value = true;
|
||||
|
||||
const menu: MenuItem[] = [];
|
||||
@@ -173,8 +184,6 @@ function show() {
|
||||
onClosing: () => {
|
||||
opening.value = false;
|
||||
},
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -225,6 +234,10 @@ function show() {
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> .inputCore {
|
||||
border-color: var(--inputBorderHover) !important;
|
||||
|
@@ -6,10 +6,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="openOnRemote" class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
|
||||
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
|
||||
</MkButton>
|
||||
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
|
||||
{{ i18n.ts.specifyServerHost }}
|
||||
</button>
|
||||
</div>
|
||||
<div :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
@@ -28,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ i18n.ts.retry }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="user && user.securityKeys" class="or-hr">
|
||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
||||
<div v-if="user && user.securityKeys" :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group _gaps">
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
@@ -53,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
@@ -60,6 +74,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||
import { host as configHost } from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { query, extractDomain } from '@/scripts/url.js';
|
||||
import { login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
@@ -72,28 +87,22 @@ const host = ref(toUnicode(configHost));
|
||||
const totpLogin = ref(false);
|
||||
const isBackupCode = ref(false);
|
||||
const queryingKey = ref(false);
|
||||
const credentialRequest = ref<CredentialRequestOptions | null>(null);
|
||||
let credentialRequest: CredentialRequestOptions | null = null;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'login', v: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
withAvatar?: boolean;
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
openOnRemote?: OpenOnRemoteOptions,
|
||||
}>(), {
|
||||
withAvatar: true,
|
||||
autoSet: false,
|
||||
message: '',
|
||||
openOnRemote: undefined,
|
||||
});
|
||||
|
||||
function onUsernameChange(): void {
|
||||
@@ -113,14 +122,14 @@ function onLogin(res: any): Promise<void> | void {
|
||||
}
|
||||
|
||||
async function queryKey(): Promise<void> {
|
||||
if (credentialRequest.value == null) return;
|
||||
if (credentialRequest == null) return;
|
||||
queryingKey.value = true;
|
||||
await webAuthnRequest(credentialRequest.value)
|
||||
await webAuthnRequest(credentialRequest)
|
||||
.catch(() => {
|
||||
queryingKey.value = false;
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
credentialRequest.value = null;
|
||||
credentialRequest = null;
|
||||
queryingKey.value = false;
|
||||
signing.value = true;
|
||||
return misskeyApi('signin', {
|
||||
@@ -151,7 +160,7 @@ function onSubmit(): void {
|
||||
}).then(res => {
|
||||
totpLogin.value = true;
|
||||
signing.value = false;
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
credentialRequest = parseRequestOptionsFromJSON({
|
||||
publicKey: res,
|
||||
});
|
||||
})
|
||||
@@ -222,6 +231,62 @@ function resetPassword(): void {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
|
||||
switch (options.type) {
|
||||
case 'web':
|
||||
case 'lookup': {
|
||||
let _path: string;
|
||||
|
||||
if (options.type === 'lookup') {
|
||||
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
|
||||
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
|
||||
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
|
||||
} else {
|
||||
_path = options.path;
|
||||
}
|
||||
|
||||
if (targetHost) {
|
||||
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
|
||||
} else {
|
||||
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
const params = query(options.params);
|
||||
if (targetHost) {
|
||||
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
|
||||
} else {
|
||||
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
|
||||
const { canceled, result: hostTemp } = await os.inputText({
|
||||
title: i18n.ts.inputHostName,
|
||||
placeholder: 'misskey.example.com',
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
let targetHost: string | null = hostTemp;
|
||||
|
||||
// ドメイン部分だけを取り出す
|
||||
targetHost = extractDomain(targetHost);
|
||||
if (targetHost == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.invalidValue,
|
||||
text: i18n.ts.tryAgain,
|
||||
});
|
||||
return;
|
||||
}
|
||||
openRemote(options, targetHost);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -234,4 +299,36 @@ function resetPassword(): void {
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.instanceManualSelectButton {
|
||||
display: block;
|
||||
text-align: center;
|
||||
opacity: .7;
|
||||
font-size: .8em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.orHr {
|
||||
position: relative;
|
||||
margin: .4em auto;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
.orMsg {
|
||||
position: absolute;
|
||||
top: -.6em;
|
||||
display: inline-block;
|
||||
padding: 0 1em;
|
||||
background: var(--panel);
|
||||
font-size: 0.8em;
|
||||
color: var(--fgOnPanel);
|
||||
margin: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
|
@@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
:width="400"
|
||||
:height="430"
|
||||
@close="onClose"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
||||
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
|
||||
withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
openOnRemote?: OpenOnRemoteOptions,
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
message: '',
|
||||
openOnRemote: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@@ -10,15 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div class="items">
|
||||
<template v-for="(item, i) in group.items">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</MkA>
|
||||
@@ -67,6 +67,10 @@ defineProps<{
|
||||
background: var(--panelHighlight);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--accentedBg);
|
||||
|
@@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
type="checkbox"
|
||||
:disabled="disabled"
|
||||
:class="$style.input"
|
||||
@keydown.enter="toggle"
|
||||
@click="toggle"
|
||||
>
|
||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||
<XButton :class="$style.toggle" :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||
<span v-if="!noBody" :class="$style.body">
|
||||
<!-- TODO: 無名slotの方は廃止 -->
|
||||
<span :class="$style.label">
|
||||
@@ -75,7 +75,13 @@ const toggle = () => {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus-visible ~ .toggle {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-left: 12px;
|
||||
margin-top: 2px;
|
||||
|
@@ -24,22 +24,23 @@ export type MkSystemWebhookResult = {
|
||||
};
|
||||
|
||||
export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
|
||||
const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
|
||||
const { dispose: _dispose } = os.popup(
|
||||
const { result } = await new Promise<{ result: MkSystemWebhookResult | null }>(async resolve => {
|
||||
const { dispose } = os.popup(
|
||||
defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
|
||||
props,
|
||||
{
|
||||
submitted: (ev: MkSystemWebhookResult) => {
|
||||
resolve({ dispose: _dispose, result: ev });
|
||||
resolve({ result: ev });
|
||||
},
|
||||
canceled: () => {
|
||||
resolve({ result: null });
|
||||
},
|
||||
closed: () => {
|
||||
resolve({ dispose: _dispose, result: null });
|
||||
dispose();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:width="450"
|
||||
:height="590"
|
||||
:canClose="true"
|
||||
@@ -12,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:okButtonDisabled="false"
|
||||
@click="onCancelClicked"
|
||||
@close="onCancelClicked"
|
||||
@closed="onCancelClicked"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
|
||||
@@ -59,8 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import {
|
||||
@@ -82,9 +82,12 @@ type EventType = {
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'submitted', result: MkSystemWebhookResult): void;
|
||||
(ev: 'canceled'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const props = defineProps<MkSystemWebhookEditorProps>();
|
||||
|
||||
const { mode, id, requiredEvents } = toRefs(props);
|
||||
@@ -133,12 +136,14 @@ async function onSubmitClicked() {
|
||||
switch (mode.value) {
|
||||
case 'create': {
|
||||
const result = await misskeyApi('admin/system-webhook/create', params);
|
||||
dialogEl.value?.close();
|
||||
emit('submitted', result);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
// eslint-disable-next-line
|
||||
const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
|
||||
dialogEl.value?.close();
|
||||
emit('submitted', result);
|
||||
break;
|
||||
}
|
||||
@@ -147,13 +152,15 @@ async function onSubmitClicked() {
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCancelClicked() {
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
|
||||
async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
||||
@@ -183,11 +190,12 @@ onMounted(async () => {
|
||||
for (const ev of Object.keys(events.value)) {
|
||||
events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: calc(100% - 38px);
|
||||
|
@@ -115,7 +115,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: calc(100% - 38px);
|
||||
|
@@ -56,7 +56,7 @@ import { i18n } from '@/i18n.js';
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: calc(100% - 38px);
|
||||
|
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
scrolling="no"
|
||||
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||
:class="$style.playerIframe"
|
||||
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
||||
:src="transformPlayerUrl(player.url)"
|
||||
:style="{ border: 0 }"
|
||||
></iframe>
|
||||
<span v-else>invalid url</span>
|
||||
@@ -91,6 +91,7 @@ import * as os from '@/os.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
|
||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_panel" :class="$style.root">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator/>
|
||||
<div :class="$style.title">
|
||||
<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
||||
<MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,6 +41,8 @@ import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
|
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
|
||||
<div v-if="user != null">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
||||
</div>
|
||||
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
|
||||
@@ -67,6 +67,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
|
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
@@ -91,7 +91,7 @@ const host = ref('');
|
||||
const users = ref<Misskey.entities.UserLite[]>([]);
|
||||
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
const selected = ref<Misskey.entities.UserLite | null>(null);
|
||||
const dialogEl = ref();
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
function search() {
|
||||
if (username.value === '' && host.value === '') {
|
||||
@@ -122,7 +122,7 @@ async function ok() {
|
||||
});
|
||||
emit('ok', user);
|
||||
|
||||
dialogEl.value.close();
|
||||
dialogEl.value?.close();
|
||||
|
||||
// 最近使ったユーザー更新
|
||||
let recents = defaultStore.state.recentlyUsedUsers;
|
||||
@@ -133,7 +133,7 @@ async function ok() {
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value.close();
|
||||
dialogEl.value?.close();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
||||
<div :class="[$style.label, $style.item]">
|
||||
{{ i18n.ts.visibility }}
|
||||
|
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div class="_gaps_s" :class="$style.mainActions">
|
||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
|
||||
import { openInstanceMenu } from '@/ui/_common_/common';
|
||||
import { openInstanceMenu } from '@/ui/_common_/common.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
|
||||
@@ -89,13 +90,9 @@ function signup() {
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu(ev) {
|
||||
function showMenu(ev: MouseEvent) {
|
||||
openInstanceMenu(ev);
|
||||
}
|
||||
|
||||
function exploreOtherServers() {
|
||||
window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="poamfof">
|
||||
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
|
||||
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
<iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
</div>
|
||||
<span v-else>invalid url</span>
|
||||
</Transition>
|
||||
@@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref } from 'vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@@ -16,7 +16,7 @@ export type MkABehavior = 'window' | 'browser' | null;
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, shallowRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
@@ -31,7 +31,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
@@ -14,7 +14,7 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
@@ -65,7 +65,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||
const validTime = (t: string | boolean | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
if (typeof t === 'boolean') return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
return t.match(/^\-?[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
const validColor = (c: unknown): string | null => {
|
||||
|
@@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div ref="headerEl">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
||||
<div
|
||||
ref="bodyEl"
|
||||
:data-sticky-container-header-height="headerHeight"
|
||||
:data-sticky-container-footer-height="footerHeight"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div ref="footerEl">
|
||||
|
@@ -53,14 +53,14 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||
const current = resolveNested(router.current)!;
|
||||
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||
const currentPageProps = ref(current.props);
|
||||
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
||||
const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props)));
|
||||
|
||||
function onChange({ resolved, key: newKey }) {
|
||||
const current = resolveNested(resolved);
|
||||
if (current == null || 'redirect' in current.route) return;
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||
key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
nextTick(() => {
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
|
@@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'alwaysMarkNsfw',
|
||||
'canUpdateBioMedia',
|
||||
'pinLimit',
|
||||
'antennaLimit',
|
||||
'wordMuteLimit',
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Directive } from 'vue';
|
||||
import { makeHotkey } from '../scripts/hotkey.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
@@ -13,9 +13,9 @@ export default {
|
||||
el._keyHandler = makeHotkey(binding.value);
|
||||
|
||||
if (el._hotkey_global) {
|
||||
document.addEventListener('keydown', el._keyHandler);
|
||||
document.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||
} else {
|
||||
el.addEventListener('keydown', el._keyHandler);
|
||||
el.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||
|
||||
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
||||
import { Component, markRaw, Ref, ref, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||
@@ -22,8 +22,11 @@ import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
@@ -622,31 +625,33 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
|
||||
align?: string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
onClosing?: () => void;
|
||||
}): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
|
||||
return new Promise(resolve => nextTick(() => {
|
||||
const { dispose } = popup(MkPopupMenu, {
|
||||
items,
|
||||
src,
|
||||
width: options?.width,
|
||||
align: options?.align,
|
||||
viaKeyboard: options?.viaKeyboard,
|
||||
returnFocusTo,
|
||||
}, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
returnFocusTo = null;
|
||||
},
|
||||
closing: () => {
|
||||
if (options?.onClosing) options.onClosing();
|
||||
options?.onClosing?.();
|
||||
},
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
||||
let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
|
||||
ev.preventDefault();
|
||||
return new Promise(resolve => {
|
||||
return new Promise(resolve => nextTick(() => {
|
||||
const { dispose } = popup(MkContextMenu, {
|
||||
items,
|
||||
ev,
|
||||
@@ -654,14 +659,28 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
|
||||
// MkModalを通していないのでここでフォーカスを戻す処理を行う
|
||||
if (returnFocusTo != null) {
|
||||
focusParent(returnFocusTo, true, false);
|
||||
returnFocusTo = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
showMovedDialog();
|
||||
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
|
||||
type: 'share',
|
||||
params: {
|
||||
text: props.initialText ?? props.initialNote.text,
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||
},
|
||||
} : undefined));
|
||||
|
||||
showMovedDialog();
|
||||
return new Promise(resolve => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
||||
|
@@ -243,6 +243,21 @@ const patronsWithIcon = [{
|
||||
}, {
|
||||
name: '越貝鯛丸',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
|
||||
}, {
|
||||
name: '☔あめ🍬(灬˘╰╯˘灬)',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/676eea72d4884d3f89aababbb62533fb.jpg',
|
||||
}, {
|
||||
name: '貯水よび',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/2974506d53244bbe94a67707b27099e2.jpg',
|
||||
}, {
|
||||
name: 'はるかさ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/26ce2432739a400aa3aa0de0ef67a107.jpg',
|
||||
}, {
|
||||
name: '天鈴のあ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/995cdbb00bd6421184461a883adfe1d9.jpg',
|
||||
}, {
|
||||
name: 'えとゔぁす',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/2578f441b82a44cfaa55ba83a318b26e.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
@@ -347,6 +362,7 @@ const patrons = [
|
||||
'SHO SEKIGUCHI',
|
||||
'塩キャベツ',
|
||||
'はとぽぷさん',
|
||||
'100の人 (エスパー・イーシア)',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
205
packages/frontend/src/pages/about.overview.vue
Normal file
205
packages/frontend/src/pages/about.overview.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutMisskey }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
</FormLink>
|
||||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue :copy="instance.maintainerName">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>
|
||||
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="instance.maintainerEmail">
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>
|
||||
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<template #value>
|
||||
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<div class="_gaps_s">
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
|
||||
<template #icon><i class="ti ti-user-shield"></i></template>
|
||||
<template #default>{{ i18n.ts.impressum }}</template>
|
||||
</FormLink>
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #icon><i class="ti ti-checkup-list"></i></template>
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
|
||||
<div :class="$style.ruleText" v-html="item"></div>
|
||||
</li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
<template #default>{{ i18n.ts.termsOfService }}</template>
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
|
||||
<template #icon><i class="ti ti-shield-lock"></i></template>
|
||||
<template #default>{{ i18n.ts.privacyPolicy }}</template>
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
|
||||
<template #icon><i class="ti ti-message"></i></template>
|
||||
<template #default>{{ i18n.ts.feedback }}</template>
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense v-slot="{ result: stats }" :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
|
||||
<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
|
||||
<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
|
||||
<FormLink to="/robots.txt" external>robots.txt</FormLink>
|
||||
<FormLink to="/manifest.json" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { host, version } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
const initStats = () => misskeyApi('stats', {});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
background-color: var(--panel);
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
display: block;
|
||||
margin: 16px auto 0 auto;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bannerName {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
.rules {
|
||||
counter-reset: item;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
word-break: break-word;
|
||||
|
||||
&::before {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 8px);
|
||||
counter-increment: item;
|
||||
content: counter(item);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
@@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutMisskey }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
</FormLink>
|
||||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
|
||||
<template #icon><i class="ti ti-user-shield"></i></template>
|
||||
{{ i18n.ts.impressum }}
|
||||
</FormLink>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #label>
|
||||
<i class="ti ti-checkup-list"></i>
|
||||
{{ i18n.ts.serverRules }}
|
||||
</template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
{{ i18n.ts.termsOfService }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
|
||||
<template #icon><i class="ti ti-shield-lock"></i></template>
|
||||
{{ i18n.ts.privacyPolicy }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
|
||||
<template #icon><i class="ti ti-message"></i></template>
|
||||
{{ i18n.ts.feedback }}
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
<XOverview/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
@@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
import { version, host } from '@/config.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
|
||||
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
|
||||
const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
|
||||
const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
@@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
|
||||
initialTab: 'overview',
|
||||
});
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
const tab = ref(props.initialTab);
|
||||
|
||||
watch(tab, () => {
|
||||
@@ -166,11 +49,6 @@ watch(tab, () => {
|
||||
}
|
||||
});
|
||||
|
||||
const initStats = () => misskeyApi('stats', {
|
||||
}).then((res) => {
|
||||
stats.value = res;
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
@@ -195,64 +73,3 @@ definePageMetadata(() => ({
|
||||
icon: 'ti ti-info-circle',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
display: block;
|
||||
margin: 16px auto 0 auto;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bannerName {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
.rules {
|
||||
counter-reset: item;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
word-break: break-word;
|
||||
|
||||
&::before {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 8px);
|
||||
counter-increment: item;
|
||||
content: counter(item);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
ref="dialogEl"
|
||||
:width="400"
|
||||
:height="490"
|
||||
:withOkButton="false"
|
||||
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
@@ -88,6 +88,7 @@ type NotificationRecipientMethod = 'email' | 'webhook';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'submitted'): void;
|
||||
(ev: 'canceled'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
@@ -98,6 +99,8 @@ const props = defineProps<{
|
||||
|
||||
const { mode, id } = toRefs(props);
|
||||
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const loading = ref<number>(0);
|
||||
|
||||
const title = ref<string>('');
|
||||
@@ -166,18 +169,21 @@ async function onSubmitClicked() {
|
||||
}
|
||||
}
|
||||
|
||||
dialogEl.value?.close();
|
||||
emit('submitted');
|
||||
// eslint-disable-next-line
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCancelClicked() {
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
|
||||
async function onEditSystemWebhookClicked() {
|
||||
@@ -262,7 +268,8 @@ onMounted(async () => {
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
dialogEl.value?.close();
|
||||
emit('canceled');
|
||||
}
|
||||
} else {
|
||||
userId.value = moderators.value[0]?.id ?? null;
|
||||
@@ -296,11 +303,13 @@ onMounted(async () => {
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
min-width: 2.5em;
|
||||
min-height: 2.5em;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
@@ -108,26 +108,27 @@ async function onDeleteButtonClicked(id: string) {
|
||||
}
|
||||
|
||||
async function showEditor(mode: 'create' | 'edit', id?: string) {
|
||||
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
|
||||
const { dispose: _dispose } = os.popup(
|
||||
const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => {
|
||||
const { dispose } = os.popup(
|
||||
defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
|
||||
{
|
||||
mode,
|
||||
id,
|
||||
},
|
||||
{
|
||||
submitted: async () => {
|
||||
resolve({ dispose: _dispose, needLoad: true });
|
||||
submitted: () => {
|
||||
resolve({ needLoad: true });
|
||||
},
|
||||
canceled: () => {
|
||||
resolve({ needLoad: false });
|
||||
},
|
||||
closed: () => {
|
||||
resolve({ dispose: _dispose, needLoad: false });
|
||||
dispose();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
dispose();
|
||||
|
||||
if (needLoad) {
|
||||
await fetchRecipients();
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts.expirationDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="createCount" type="number">
|
||||
<MkInput v-model="createCount" type="number" min="1">
|
||||
<template #label>{{ i18n.ts.createCount }}</template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
|
||||
|
@@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUpdateBioMedia.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>
|
||||
|
@@ -134,6 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUpdateBioMedia">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>{{ policies.pinLimit }}</template>
|
||||
|
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div ref="rootEl" v-hotkey.global="keymap">
|
||||
<div ref="rootEl">
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
<div :class="$style.tl">
|
||||
<MkTimeline
|
||||
@@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null);
|
||||
const queue = ref(0);
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const keymap = computed(() => ({
|
||||
't': focus,
|
||||
}));
|
||||
|
||||
function queueUpdated(q) {
|
||||
queue.value = q;
|
||||
|
@@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
|
@@ -43,7 +43,7 @@ import { url } from '@/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { clipsCache } from '@/cache.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string,
|
||||
|
@@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="instance.maintainerName">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>
|
||||
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<MkKeyValue :copy="instance.maintainerEmail">
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>
|
||||
<div>{{ instance.maintainerEmail }}</div>
|
||||
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="instance.inquiryUrl">
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<template #value>
|
||||
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
@@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
|
@@ -234,6 +234,7 @@ onMounted(async () => {
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
@@ -210,7 +210,7 @@ import { apiUrl } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
type FrontendMonoDefinition = {
|
||||
id: string;
|
||||
|
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
||||
|
@@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
@@ -48,7 +49,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const PRESET_DEFAULT = `/// @ 0.18.0
|
||||
const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
|
||||
|
||||
var name = ""
|
||||
|
||||
@@ -66,7 +67,7 @@ Ui:render([
|
||||
])
|
||||
`;
|
||||
|
||||
const PRESET_OMIKUJI = `/// @ 0.18.0
|
||||
const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION}
|
||||
// ユーザーごとに日替わりのおみくじのプリセット
|
||||
|
||||
// 選択肢
|
||||
@@ -109,7 +110,7 @@ Ui:render([
|
||||
])
|
||||
`;
|
||||
|
||||
const PRESET_SHUFFLE = `/// @ 0.18.0
|
||||
const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION}
|
||||
// 巻き戻し可能な文字シャッフルのプリセット
|
||||
|
||||
let string = "ペペロンチーノ"
|
||||
@@ -188,7 +189,7 @@ var cursor = 0
|
||||
do()
|
||||
`;
|
||||
|
||||
const PRESET_QUIZ = `/// @ 0.18.0
|
||||
const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
|
||||
let title = '地理クイズ'
|
||||
|
||||
let qas = [{
|
||||
@@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({
|
||||
Ui:render(qaEls)
|
||||
`;
|
||||
|
||||
const PRESET_TIMELINE = `/// @ 0.18.0
|
||||
const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
|
||||
// APIリクエストを行いローカルタイムラインを表示するプリセット
|
||||
|
||||
@fetch() {
|
||||
|
@@ -78,7 +78,8 @@ import MkCode from '@/components/MkCode.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
@@ -143,6 +144,7 @@ function shareWithNote() {
|
||||
|
||||
function like() {
|
||||
if (!flash.value) return;
|
||||
pleaseLogin();
|
||||
|
||||
os.apiWithDialog('flash/like', {
|
||||
flashId: flash.value.id,
|
||||
@@ -154,6 +156,7 @@ function like() {
|
||||
|
||||
async function unlike() {
|
||||
if (!flash.value) return;
|
||||
pleaseLogin();
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@@ -1,71 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
|
||||
async function follow(user): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.tsx.followConfirm({ name: user.name || user.username }),
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
});
|
||||
user.withReplies = defaultStore.state.defaultWithReplies;
|
||||
}
|
||||
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null) {
|
||||
throw new Error('acct required');
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise = misskeyApi('users/show', Misskey.acct.parse(acct));
|
||||
promise.then(user => {
|
||||
follow(user);
|
||||
});
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
</script>
|
@@ -77,7 +77,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
@@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_gaps">
|
||||
<div class="_panel">
|
||||
<div class="_panel" :class="$style.link">
|
||||
<MkA to="/bubble-game">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div class="_panel" :class="$style.link">
|
||||
<MkA to="/reversi">
|
||||
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
@@ -37,3 +37,10 @@ definePageMetadata(() => ({
|
||||
icon: 'ti ti-device-gamepad',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.link:focus-within {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
|
97
packages/frontend/src/pages/lookup.vue
Normal file
97
packages/frontend/src/pages/lookup.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="state === 'done'" class="_buttonsCenter">
|
||||
<MkButton @click="close">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton>
|
||||
</div>
|
||||
<div v-else class="_fullInfo">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const state = ref<'fetching' | 'done'>('fetching');
|
||||
|
||||
function fetch() {
|
||||
const params = new URL(location.href).searchParams;
|
||||
|
||||
// acctのほうはdeprecated
|
||||
let uri = params.get('uri') ?? params.get('acct');
|
||||
if (uri == null) {
|
||||
state.value = 'done';
|
||||
return;
|
||||
}
|
||||
|
||||
let promise: Promise<any>;
|
||||
|
||||
if (uri.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
uri,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.replace(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (uri.startsWith('acct:')) {
|
||||
uri = uri.slice(5);
|
||||
}
|
||||
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
|
||||
promise.then(user => {
|
||||
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
|
||||
});
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
window.close();
|
||||
|
||||
// 閉じなければ100ms後タイムラインに
|
||||
window.setTimeout(() => {
|
||||
location.href = '/';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function goToMisskey(): void {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
fetch();
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.lookup,
|
||||
icon: 'ti ti-world-search',
|
||||
});
|
||||
</script>
|
@@ -125,7 +125,7 @@ import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
@@ -286,6 +286,7 @@ definePageMetadata(() => ({
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, ref, watch, type StyleValue } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
@@ -102,10 +102,10 @@ function fetchDriveInfo(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function genUsageBar(fsize: number): object {
|
||||
function genUsageBar(fsize: number): StyleValue {
|
||||
return {
|
||||
width: `${fsize / usage.value * 100}%`,
|
||||
background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }),
|
||||
background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }).toHslString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -95,7 +95,7 @@ const meterStyle = computed(() => {
|
||||
h: 180 - (usage.value / capacity.value * 180),
|
||||
s: 0.7,
|
||||
l: 0.5,
|
||||
}),
|
||||
}).toHslString(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@@ -169,6 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
|
||||
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
|
||||
<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
@@ -315,6 +316,7 @@ const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enabl
|
||||
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
|
||||
const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
|
||||
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
|
||||
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
@@ -357,6 +359,7 @@ watch([
|
||||
disableStreamingTimeline,
|
||||
enableSeasonalScreenEffect,
|
||||
alwaysConfirmFollow,
|
||||
confirmWhenRevealingSensitiveMedia,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
@@ -82,7 +82,7 @@ import MkCode from '@/components/MkCode.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { ColdDeviceStorage } from '@/store.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@@ -113,8 +113,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||
'sound_note',
|
||||
'sound_noteMy',
|
||||
'sound_notification',
|
||||
'sound_antenna',
|
||||
'sound_channel',
|
||||
];
|
||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
||||
'lightTheme',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user