Merge branch 'develop' into pr/ThatOneCalculator/8764
This commit is contained in:
commit
7ddfd049a4
234 changed files with 7586 additions and 783 deletions
|
|
@ -15,6 +15,12 @@ module.exports = {
|
|||
'plugin:vue/vue3-recommended',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
'allowSingleExtends': true,
|
||||
},
|
||||
],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// data の禁止理由: 抽象的すぎるため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@
|
|||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@vue/compiler-sfc": "3.2.36",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.12.0",
|
||||
"broadcast-channel": "4.13.0",
|
||||
"browser-image-resizer": "misskey-dev/browser-image-resizer#tag=v2.2.1-misskey.2",
|
||||
"chart.js": "3.8.0",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "4.1.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"cropperjs": "2.0.0-beta",
|
||||
"date-fns": "2.28.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
|
|
@ -58,9 +59,9 @@
|
|||
"random-seed": "0.3.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "2.75.3",
|
||||
"rollup": "2.75.6",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.52.1",
|
||||
"sass": "1.52.3",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
|
|
@ -69,20 +70,20 @@
|
|||
"three": "0.141.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.6.7",
|
||||
"tsc-alias": "1.6.9",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "4.7.2",
|
||||
"typescript": "4.7.3",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vite": "2.9.9",
|
||||
"vue": "3.2.36",
|
||||
"vite": "2.9.10",
|
||||
"vue": "3.2.37",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.15",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vue-router": "4.0.16",
|
||||
"vuedraggable": "4.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.7.0"
|
||||
"ws": "8.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
|
|
@ -103,13 +104,13 @@
|
|||
"@types/uuid": "8.3.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.27.0",
|
||||
"@typescript-eslint/parser": "5.27.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.27.1",
|
||||
"@typescript-eslint/parser": "5.27.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.7.0",
|
||||
"eslint": "8.16.0",
|
||||
"cypress": "10.0.3",
|
||||
"eslint": "8.17.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.0.1",
|
||||
"eslint-plugin-vue": "9.1.0",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ type CaptchaContainer = {
|
|||
};
|
||||
|
||||
declare global {
|
||||
interface Window extends CaptchaContainer {
|
||||
}
|
||||
interface Window extends CaptchaContainer { }
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
175
packages/client/src/components/cropper-dialog.vue
Normal file
175
packages/client/src/components/cropper-dialog.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
ref="dialogEl"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:scroll="false"
|
||||
:with-ok-button="true"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>{{ $ts.cropImage }}</template>
|
||||
<template #default="{ width, height }">
|
||||
<div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`">
|
||||
<Transition name="fade">
|
||||
<div v-if="loading" class="loading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
import { apiUrl, url } from '@/config';
|
||||
import { query } from '@/scripts/url';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', cropped: misskey.entities.DriveFile): void;
|
||||
(ev: 'cancel'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
file: misskey.entities.DriveFile;
|
||||
aspectRatio: number;
|
||||
}>();
|
||||
|
||||
const imgUrl = `${url}/proxy/image.webp?${query({
|
||||
url: props.file.url,
|
||||
})}`;
|
||||
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
|
||||
let imgEl = $ref<HTMLImageElement>();
|
||||
let cropper: Cropper | null = null;
|
||||
let loading = $ref(true);
|
||||
|
||||
const ok = async () => {
|
||||
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
|
||||
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
||||
croppedCanvas.toBlob(blob => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob);
|
||||
formData.append('i', $i.token);
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||
}
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
res(f);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
os.promiseDialog(promise);
|
||||
|
||||
const f = await promise;
|
||||
|
||||
emit('ok', f);
|
||||
dialogEl.close();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('cancel');
|
||||
dialogEl.close();
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
loading = false;
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
cropper.getCropperSelection()!.$center();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
cropper = new Cropper(imgEl, {
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
const selection = cropper.getCropperSelection()!;
|
||||
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
|
||||
selection.aspectRatio = props.aspectRatio;
|
||||
selection.initialAspectRatio = props.aspectRatio;
|
||||
selection.outlined = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
selection.$center();
|
||||
}, 100);
|
||||
|
||||
// モーダルオープンアニメーションが終わったあとで再度調整
|
||||
window.setTimeout(() => {
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
selection.$center();
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s ease 0.5s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-cropper-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--vw);
|
||||
height: var(--vh);
|
||||
position: relative;
|
||||
|
||||
> .loading {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||
backdrop-filter: var(--blur, blur(10px));
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
> .container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> ::v-deep(cropper-canvas) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> cropper-selection > cropper-handle[action="move"] {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div ref="thumbnail" class="zdjebgpv">
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
||||
<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i>
|
||||
<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i>
|
||||
<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i>
|
||||
|
|
@ -33,16 +33,16 @@ const is = computed(() => {
|
|||
if (props.file.type.endsWith('/pdf')) return 'pdf';
|
||||
if (props.file.type.startsWith('text/')) return 'textfile';
|
||||
if ([
|
||||
"application/zip",
|
||||
"application/x-cpio",
|
||||
"application/x-bzip",
|
||||
"application/x-bzip2",
|
||||
"application/java-archive",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-tar",
|
||||
"application/gzip",
|
||||
"application/x-7z-compressed"
|
||||
].some(archiveType => archiveType === props.file.type)) return 'archive';
|
||||
'application/zip',
|
||||
'application/x-cpio',
|
||||
'application/x-bzip',
|
||||
'application/x-bzip2',
|
||||
'application/java-archive',
|
||||
'application/x-rar-compressed',
|
||||
'application/x-tar',
|
||||
'application/gzip',
|
||||
'application/x-7z-compressed',
|
||||
].some(archiveType => archiveType === props.file.type)) return 'archive';
|
||||
return 'unknown';
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ function onMouseover() {
|
|||
}
|
||||
|
||||
function onMouseout() {
|
||||
hover.value = false
|
||||
hover.value = false;
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
|
|
@ -204,7 +204,7 @@ function deleteFolder() {
|
|||
defaultStore.set('uploadFolder', null);
|
||||
}
|
||||
}).catch(err => {
|
||||
switch(err.id) {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
os.alert({
|
||||
type: 'error',
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ const fetching = ref(true);
|
|||
|
||||
const ilFilesObserver = new IntersectionObserver(
|
||||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
|
||||
)
|
||||
);
|
||||
|
||||
watch(folder, () => emit('cd', folder.value));
|
||||
|
||||
|
|
@ -332,7 +332,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
|||
// 削除時に親フォルダに移動
|
||||
move(folderToDelete.parentId);
|
||||
}).catch(err => {
|
||||
switch(err.id) {
|
||||
switch (err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
os.alert({
|
||||
type: 'error',
|
||||
|
|
@ -607,7 +607,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||
onMounted(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el)
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -628,7 +628,7 @@ onMounted(() => {
|
|||
onActivated(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el)
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
|
||||
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
|
||||
<div ref="emojis" class="emojis">
|
||||
<section class="result">
|
||||
<div v-if="searchResultCustom.length > 0">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
|
|
@ -43,32 +43,30 @@ const props = withDefaults(defineProps<{
|
|||
large: false,
|
||||
});
|
||||
|
||||
const isFollowing = ref(props.user.isFollowing);
|
||||
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
|
||||
const wait = ref(false);
|
||||
let isFollowing = $ref(props.user.isFollowing);
|
||||
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
|
||||
let wait = $ref(false);
|
||||
const connection = stream.useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
os.api('users/show', {
|
||||
userId: props.user.id
|
||||
}).then(u => {
|
||||
isFollowing.value = u.isFollowing;
|
||||
hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
|
||||
});
|
||||
})
|
||||
.then(onFollowChange);
|
||||
}
|
||||
|
||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
if (user.id === props.user.id) {
|
||||
isFollowing.value = user.isFollowing;
|
||||
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
|
||||
isFollowing = user.isFollowing;
|
||||
hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
wait.value = true;
|
||||
wait = true;
|
||||
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
if (isFollowing) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
|
||||
|
|
@ -80,26 +78,22 @@ async function onClick() {
|
|||
userId: props.user.id
|
||||
});
|
||||
} else {
|
||||
if (hasPendingFollowRequestFromYou.value) {
|
||||
if (hasPendingFollowRequestFromYou) {
|
||||
await os.api('following/requests/cancel', {
|
||||
userId: props.user.id
|
||||
});
|
||||
} else if (props.user.isLocked) {
|
||||
await os.api('following/create', {
|
||||
userId: props.user.id
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
hasPendingFollowRequestFromYou = false;
|
||||
} else {
|
||||
await os.api('following/create', {
|
||||
userId: props.user.id
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
hasPendingFollowRequestFromYou = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
wait = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
|
|||
defaultOpen: boolean;
|
||||
}>(), {
|
||||
defaultOpen: false,
|
||||
})
|
||||
});
|
||||
|
||||
let opened = $ref(props.defaultOpen);
|
||||
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
|
||||
<video
|
||||
:poster="video.thumbnailUrl"
|
||||
:title="video.name"
|
||||
:title="video.comment"
|
||||
:alt="video.comment"
|
||||
preload="none"
|
||||
controls
|
||||
@contextmenu.stop
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ export default defineComponent({
|
|||
|
||||
inject: {
|
||||
sideViewHook: {
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
provide() {
|
||||
|
|
@ -94,31 +94,31 @@ export default defineComponent({
|
|||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: this.$ts.showInPage,
|
||||
action: this.expand
|
||||
action: this.expand,
|
||||
}, this.sideViewHook ? {
|
||||
icon: 'fas fa-columns',
|
||||
text: this.$ts.openInSideView,
|
||||
action: () => {
|
||||
this.sideViewHook(this.path);
|
||||
this.$refs.window.close();
|
||||
}
|
||||
},
|
||||
} : undefined, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.popout,
|
||||
action: this.popout
|
||||
action: this.popout,
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.$refs.window.close();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: this.$ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
},
|
||||
}];
|
||||
},
|
||||
},
|
||||
|
|
@ -155,7 +155,7 @@ export default defineComponent({
|
|||
|
||||
onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(this.contextmenu, ev);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ function react(viaKeyboard = false): void {
|
|||
reactionPicker.show(reactButton.value, reaction => {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction
|
||||
reaction: reaction,
|
||||
});
|
||||
}, () => {
|
||||
focus();
|
||||
|
|
@ -233,7 +233,7 @@ function undoReact(note): void {
|
|||
const oldReaction = note.myReaction;
|
||||
if (!oldReaction) return;
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: note.id
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
|
||||
viaKeyboard
|
||||
viaKeyboard,
|
||||
}).then(focus);
|
||||
}
|
||||
|
||||
|
|
@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void {
|
|||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: note.id
|
||||
noteId: note.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
},
|
||||
}], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -288,14 +288,14 @@ function blur() {
|
|||
|
||||
os.api('notes/children', {
|
||||
noteId: appearNote.id,
|
||||
limit: 30
|
||||
limit: 30,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
});
|
||||
|
||||
if (appearNote.replyId) {
|
||||
os.api('notes/conversation', {
|
||||
noteId: appearNote.replyId
|
||||
noteId: appearNote.replyId,
|
||||
}).then(res => {
|
||||
conversation.value = res.reverse();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<span v-if="note.cw != ''" class="text">{{ note.cw }}</span>
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ function react(viaKeyboard = false): void {
|
|||
reactionPicker.show(reactButton.value, reaction => {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction
|
||||
reaction: reaction,
|
||||
});
|
||||
}, () => {
|
||||
focus();
|
||||
|
|
@ -221,7 +221,7 @@ function undoReact(note): void {
|
|||
const oldReaction = note.myReaction;
|
||||
if (!oldReaction) return;
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: note.id
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
|
||||
viaKeyboard
|
||||
viaKeyboard,
|
||||
}).then(focus);
|
||||
}
|
||||
|
||||
|
|
@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void {
|
|||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: note.id
|
||||
noteId: note.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
},
|
||||
}], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +284,7 @@ function focusAfter() {
|
|||
|
||||
function readPromo() {
|
||||
os.api('promo/read', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<XModalWindow ref="dialog"
|
||||
<XModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
|
|
@ -28,18 +29,18 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInfo from './ui/info.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModalWindow,
|
||||
MkSwitch,
|
||||
MkInfo,
|
||||
MkButton
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -53,7 +54,7 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
|
@ -93,7 +94,7 @@ export default defineComponent({
|
|||
for (const type in this.typesMap) {
|
||||
this.typesMap[type as typeof notificationTypes[number]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
|
||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||
<XReactionIcon v-else-if="notification.type === 'reaction'"
|
||||
<XReactionIcon
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
ref="reactionRef"
|
||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||
:custom-emojis="notification.note.emojis"
|
||||
|
|
@ -74,10 +75,10 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
import XReactionIcon from './reaction-icon.vue';
|
||||
import MkFollowButton from './follow-button.vue';
|
||||
import XReactionTooltip from './reaction-tooltip.vue';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { userPage } from '@/filters/user';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XReactionIcon, MkFollowButton
|
||||
XReactionIcon, MkFollowButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -116,7 +117,7 @@ export default defineComponent({
|
|||
const readObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (!entries.some(entry => entry.isIntersecting)) return;
|
||||
stream.send('readNotification', {
|
||||
id: props.notification.id
|
||||
id: props.notification.id,
|
||||
});
|
||||
observer.disconnect();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { Paging } from '@/components/ui/pagination.vue';
|
||||
import MkPagination, { Paging } from '@/components/ui/pagination.vue';
|
||||
import XNotification from '@/components/notification.vue';
|
||||
import XList from '@/components/date-separated-list.vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
|
|
@ -49,14 +48,14 @@ const onNotification = (notification) => {
|
|||
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
||||
if (isMuted || document.visibilityState === 'visible') {
|
||||
stream.send('readNotification', {
|
||||
id: notification.id
|
||||
id: notification.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMuted) {
|
||||
pagingComponent.value.prepend({
|
||||
...notification,
|
||||
isRead: document.visibilityState === 'visible'
|
||||
isRead: document.visibilityState === 'visible',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default defineComponent({
|
|||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export default defineComponent({
|
|||
isZero,
|
||||
number,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default defineComponent({
|
|||
.then(response => response.json())
|
||||
.then(f => {
|
||||
ok(f);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
os.promiseDialog(promise);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
|
||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -114,19 +114,19 @@ export default defineComponent({
|
|||
this.menu = os.popupMenu([{
|
||||
text: this.$ts.renameFile,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: () => { this.rename(file) }
|
||||
action: () => { this.rename(file); }
|
||||
}, {
|
||||
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
|
||||
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
|
||||
action: () => { this.toggleSensitive(file) }
|
||||
action: () => { this.toggleSensitive(file); }
|
||||
}, {
|
||||
text: this.$ts.describeFile,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: () => { this.describe(file) }
|
||||
action: () => { this.describe(file); }
|
||||
}, {
|
||||
text: this.$ts.attachCancel,
|
||||
icon: 'fas fa-times-circle',
|
||||
action: () => { this.detachMedia(file.id) }
|
||||
action: () => { this.detachMedia(file.id); }
|
||||
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ function onCompositionEnd(ev: CompositionEvent) {
|
|||
}
|
||||
|
||||
async function onPaste(ev: ClipboardEvent) {
|
||||
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({item, i}))) {
|
||||
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({ item, i }))) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
const lio = file.name.lastIndexOf('.');
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
chartEl,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default defineComponent({
|
|||
flag: true,
|
||||
radio: 'misskey',
|
||||
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ function queryKey() {
|
|||
|
||||
function onSubmit() {
|
||||
signing = true;
|
||||
console.log('submit')
|
||||
console.log('submit');
|
||||
if (!totpLogin && user && user.twoFactorEnabled) {
|
||||
if (window.PublicKeyCredential && user.securityKeys) {
|
||||
os.api('signin', {
|
||||
|
|
@ -222,7 +222,7 @@ function loginFailed(err) {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(err)
|
||||
console.log(err);
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<form class="qlvuhzng _formRoot" :autocomplete="Math.random()" @submit.prevent="onSubmit">
|
||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<template v-if="meta">
|
||||
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :autocomplete="Math.random()" spellcheck="false" required>
|
||||
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required>
|
||||
<template #label>{{ $ts.invitationCode }}</template>
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>{{ $ts.password }}</template>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -111,7 +111,7 @@ export default defineComponent({
|
|||
ToSAgreement: false,
|
||||
hCaptchaResponse: null,
|
||||
reCaptchaResponse: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
|
||||
const origin = {x: circleCenterX, y: circleCenterY};
|
||||
const dist1 = distance({x: 0, y: 0}, origin);
|
||||
const dist2 = distance({x: boxW, y: 0}, origin);
|
||||
const dist3 = distance({x: 0, y: boxH}, origin);
|
||||
const dist4 = distance({x: boxW, y: boxH }, origin);
|
||||
const origin = { x: circleCenterX, y: circleCenterY };
|
||||
const dist1 = distance({ x: 0, y: 0 }, origin);
|
||||
const dist2 = distance({ x: boxW, y: 0 }, origin);
|
||||
const dist3 = distance({ x: 0, y: boxH }, origin);
|
||||
const dist4 = distance({ x: boxW, y: boxH }, origin);
|
||||
return Math.max(dist1, dist2, dist3, dist4) * 2;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div ref="itemsEl" v-hotkey="keymap"
|
||||
<div
|
||||
ref="itemsEl" v-hotkey="keymap"
|
||||
class="rrevdjwt"
|
||||
:class="{ center: align === 'center', asDrawer }"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
|
|
@ -162,6 +163,15 @@ function focusDown() {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff2a2a;
|
||||
|
||||
|
|
@ -191,15 +201,6 @@ function focusDown() {
|
|||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
||||
&:before {
|
||||
background: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active):focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
||||
<div class="header">
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
||||
<div ref="headerEl" class="header">
|
||||
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button>
|
||||
<span class="title">
|
||||
<slot name="header"></slot>
|
||||
|
|
@ -11,82 +11,82 @@
|
|||
</div>
|
||||
<div v-if="padding" class="body">
|
||||
<div class="_section">
|
||||
<slot></slot>
|
||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="body">
|
||||
<slot></slot>
|
||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import MkModal from './modal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal
|
||||
},
|
||||
props: {
|
||||
withOkButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
okButtonDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
padding: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 400
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
scroll: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
withOkButton: boolean;
|
||||
okButtonDisabled: boolean;
|
||||
padding: boolean;
|
||||
width: number;
|
||||
height: number | null;
|
||||
scroll: boolean;
|
||||
}>(), {
|
||||
withOkButton: false,
|
||||
okButtonDisabled: false,
|
||||
padding: false,
|
||||
width: 400,
|
||||
height: null,
|
||||
scroll: true,
|
||||
});
|
||||
|
||||
emits: ['click', 'close', 'closed', 'ok'],
|
||||
const emit = defineEmits<{
|
||||
(event: 'click'): void;
|
||||
(event: 'close'): void;
|
||||
(event: 'closed'): void;
|
||||
(event: 'ok'): void;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
||||
let rootEl = $ref<HTMLElement>();
|
||||
let headerEl = $ref<HTMLElement>();
|
||||
let bodyWidth = $ref(0);
|
||||
let bodyHeight = $ref(0);
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
const close = () => {
|
||||
modal.close();
|
||||
};
|
||||
|
||||
onKeydown(evt) {
|
||||
if (evt.which === 27) { // Esc
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
const onBgClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onKeydown = (evt) => {
|
||||
if (evt.which === 27) { // Esc
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
bodyWidth = rootEl.offsetWidth;
|
||||
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
bodyWidth = rootEl.offsetWidth;
|
||||
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
|
||||
ro.observe(rootEl);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
|
||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||
|
|
@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'opening'): void;
|
||||
(ev: 'opened'): void;
|
||||
(ev: 'click'): void;
|
||||
(ev: 'esc'): void;
|
||||
(ev: 'close'): void;
|
||||
|
|
@ -212,7 +213,9 @@ const align = () => {
|
|||
popover.style.top = top + 'px';
|
||||
};
|
||||
|
||||
const childRendered = () => {
|
||||
const onOpened = () => {
|
||||
emit('opened');
|
||||
|
||||
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
|
||||
const el = content.value!.children[0];
|
||||
el.addEventListener('mousedown', ev => {
|
||||
|
|
@ -234,10 +237,10 @@ onMounted(() => {
|
|||
}
|
||||
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
|
||||
|
||||
await nextTick()
|
||||
await nextTick();
|
||||
|
||||
align();
|
||||
}, { immediate: true, });
|
||||
}, { immediate: true });
|
||||
|
||||
nextTick(() => {
|
||||
const popover = content.value;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')">
|
||||
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
|
||||
<slot>{{ text }}</slot>
|
||||
<slot>
|
||||
<Mfm v-if="asMfm" :text="text"/>
|
||||
<span v-else>{{ text }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
|
@ -16,6 +19,7 @@ const props = withDefaults(defineProps<{
|
|||
x?: number;
|
||||
y?: number;
|
||||
text?: string;
|
||||
asMfm?: boolean;
|
||||
maxWidth?: number;
|
||||
direction?: 'top' | 'bottom' | 'right' | 'left';
|
||||
innerMargin?: number;
|
||||
|
|
@ -63,7 +67,7 @@ const setPosition = () => {
|
|||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
};
|
||||
|
||||
const calcPosWhenBottom = () => {
|
||||
let left: number;
|
||||
|
|
@ -84,7 +88,7 @@ const setPosition = () => {
|
|||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
};
|
||||
|
||||
const calcPosWhenLeft = () => {
|
||||
let left: number;
|
||||
|
|
@ -105,7 +109,7 @@ const setPosition = () => {
|
|||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
};
|
||||
|
||||
const calcPosWhenRight = () => {
|
||||
let left: number;
|
||||
|
|
@ -126,7 +130,7 @@ const setPosition = () => {
|
|||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
};
|
||||
|
||||
const calc = (): {
|
||||
left: number;
|
||||
|
|
@ -170,9 +174,7 @@ const setPosition = () => {
|
|||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
}
|
||||
|
||||
return null as never;
|
||||
}
|
||||
};
|
||||
|
||||
const { left, top, transformOrigin } = calc();
|
||||
el.value.style.transformOrigin = transformOrigin;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the
|
|||
sitename = info.sitename;
|
||||
fetching = false;
|
||||
player = info.player;
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
function adjustTweetHeight(message: any) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default {
|
|||
} else {
|
||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parentBg = getBgColor(src.parentElement);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ function calc(src: Element) {
|
|||
return;
|
||||
}
|
||||
if (info.intersection) {
|
||||
info.intersection.disconnect()
|
||||
info.intersection.disconnect();
|
||||
delete info.intersection;
|
||||
};
|
||||
}
|
||||
|
||||
info.fn(width, height);
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default {
|
|||
} else {
|
||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parentBg = getBgColor(src.parentElement);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ function calc(el: Element) {
|
|||
return;
|
||||
}
|
||||
if (info.intersection) {
|
||||
info.intersection.disconnect()
|
||||
info.intersection.disconnect();
|
||||
delete info.intersection;
|
||||
};
|
||||
}
|
||||
|
||||
mountings.set(el, Object.assign(info, { previousWidth: width }));
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export default {
|
|||
popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
|
||||
showing,
|
||||
text: self.text,
|
||||
asMfm: binding.modifiers.mfm,
|
||||
targetElement: el,
|
||||
}, {}, 'closed');
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
|
|||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache'
|
||||
cache: 'no-cache',
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ export async function popup(component: Component, props: Record<string, any>, ev
|
|||
props,
|
||||
events: disposeEvent ? {
|
||||
...events,
|
||||
[disposeEvent]: dispose
|
||||
[disposeEvent]: dispose,
|
||||
} : events,
|
||||
id,
|
||||
};
|
||||
|
|
@ -174,7 +174,7 @@ export function modalPageWindow(path: string) {
|
|||
|
||||
export function toast(message: string) {
|
||||
popup(defineAsyncComponent(() => import('@/components/toast.vue')), {
|
||||
message
|
||||
message,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ export function inputText(props: {
|
|||
type: props.type,
|
||||
placeholder: props.placeholder,
|
||||
default: props.default,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
|
|
@ -251,7 +251,7 @@ export function inputNumber(props: {
|
|||
type: 'number',
|
||||
placeholder: props.placeholder,
|
||||
default: props.default,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
|
|
@ -276,7 +276,7 @@ export function inputDate(props: {
|
|||
type: 'date',
|
||||
placeholder: props.placeholder,
|
||||
default: props.default,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
|
||||
|
|
@ -285,7 +285,7 @@ export function inputDate(props: {
|
|||
});
|
||||
}
|
||||
|
||||
export function select<C extends any = any>(props: {
|
||||
export function select<C = any>(props: {
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
default?: string | null;
|
||||
|
|
@ -313,7 +313,7 @@ export function select<C extends any = any>(props: {
|
|||
items: props.items,
|
||||
groupedItems: props.groupedItems,
|
||||
default: props.default,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
|
|
@ -330,7 +330,7 @@ export function success() {
|
|||
}, 1000);
|
||||
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
|
||||
success: true,
|
||||
showing: showing
|
||||
showing: showing,
|
||||
}, {
|
||||
done: () => resolve(),
|
||||
}, 'closed');
|
||||
|
|
@ -342,7 +342,7 @@ export function waiting() {
|
|||
const showing = ref(true);
|
||||
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
|
||||
success: false,
|
||||
showing: showing
|
||||
showing: showing,
|
||||
}, {
|
||||
done: () => resolve(),
|
||||
}, 'closed');
|
||||
|
|
@ -373,7 +373,7 @@ export async function selectDriveFile(multiple: boolean) {
|
|||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
|
||||
type: 'file',
|
||||
multiple
|
||||
multiple,
|
||||
}, {
|
||||
done: files => {
|
||||
if (files) {
|
||||
|
|
@ -388,7 +388,7 @@ export async function selectDriveFolder(multiple: boolean) {
|
|||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
|
||||
type: 'folder',
|
||||
multiple
|
||||
multiple,
|
||||
}, {
|
||||
done: folders => {
|
||||
if (folders) {
|
||||
|
|
@ -403,7 +403,7 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
|
|||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
|
||||
src,
|
||||
...opts
|
||||
...opts,
|
||||
}, {
|
||||
done: emoji => {
|
||||
resolve(emoji);
|
||||
|
|
@ -412,6 +412,21 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||
aspectRatio: number;
|
||||
}): Promise<Misskey.entities.DriveFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/cropper-dialog.vue')), {
|
||||
file: image,
|
||||
aspectRatio: options.aspectRatio,
|
||||
}, {
|
||||
ok: x => {
|
||||
resolve(x);
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
type AwaitType<T> =
|
||||
T extends Promise<infer U> ? U :
|
||||
T extends (...args: any[]) => Promise<infer V> ? V :
|
||||
|
|
@ -453,7 +468,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
|||
|
||||
openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/emoji-picker-window.vue')), {
|
||||
src,
|
||||
...opts
|
||||
...opts,
|
||||
}, {
|
||||
chosen: emoji => {
|
||||
insertTextAtCursor(activeTextarea, emoji);
|
||||
|
|
@ -462,7 +477,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
|||
openingEmojiPicker!.dispose();
|
||||
openingEmojiPicker = null;
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -478,7 +493,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
|||
src,
|
||||
width: options?.width,
|
||||
align: options?.align,
|
||||
viaKeyboard: options?.viaKeyboard
|
||||
viaKeyboard: options?.viaKeyboard,
|
||||
}, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
|
|||
}, {
|
||||
text: i18n.ts.import,
|
||||
icon: 'fas fa-plus',
|
||||
action: () => { im(emoji) }
|
||||
action: () => { im(emoji); }
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +1,50 @@
|
|||
<template>
|
||||
<div class="xrmjdkdw">
|
||||
<MkContainer :foldable="true" class="lookup">
|
||||
<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
|
||||
<div class="xrmjdkdw-lookup">
|
||||
<MkInput v-model="q" class="item" type="text" @enter="find()">
|
||||
<template #label>{{ $ts.fileIdOrUrl }}</template>
|
||||
<div>
|
||||
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<MkButton primary @click="find()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="_section">
|
||||
<div class="_content">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
|
||||
<template #label>MIME type</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief">
|
||||
<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div class="body">
|
||||
<div>
|
||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<MkAcct v-if="file.user" :user="file.user"/>
|
||||
<div v-else>{{ $ts.system }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||
</div>
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
|
||||
<template #label>MIME type</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div v-if="viewMode === 'list'" class="body">
|
||||
<div>
|
||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div>
|
||||
<MkAcct v-if="file.user" :user="file.user"/>
|
||||
<div v-else>{{ $ts.system }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
|
|
@ -67,10 +56,10 @@ import * as os from '@/os';
|
|||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let q = $ref(null);
|
||||
let origin = $ref('local');
|
||||
let type = $ref(null);
|
||||
let searchHost = $ref('');
|
||||
let viewMode = $ref('grid');
|
||||
const pagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
|
|
@ -94,18 +83,24 @@ function clear() {
|
|||
|
||||
function show(file) {
|
||||
os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), {
|
||||
fileId: file.id
|
||||
fileId: file.id,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function find() {
|
||||
async function find() {
|
||||
const { canceled, result: q } = await os.inputText({
|
||||
title: i18n.ts.fileIdOrUrl,
|
||||
allowEmpty: false,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound
|
||||
text: i18n.ts.notFound,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -117,6 +112,10 @@ defineExpose({
|
|||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'fas fa-search',
|
||||
handler: find,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'fas fa-trash-alt',
|
||||
handler: clear,
|
||||
|
|
@ -129,47 +128,53 @@ defineExpose({
|
|||
.xrmjdkdw {
|
||||
margin: var(--margin);
|
||||
|
||||
> .lookup {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.urempief {
|
||||
margin-top: var(--margin);
|
||||
|
||||
> .file {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
&.list {
|
||||
> .file {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
margin-left: 0.3em;
|
||||
padding: 8px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
&.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .body {
|
||||
margin-left: 0.3em;
|
||||
padding: 8px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 14px;
|
||||
> .file {
|
||||
aspect-ratio: 1;
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xrmjdkdw-lookup {
|
||||
padding: 16px;
|
||||
|
||||
> .item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export default defineComponent({
|
|||
overviewHeight: '1fr',
|
||||
queueHeight: '1fr',
|
||||
paused: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import * as symbols from '@/symbols';
|
|||
import * as config from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const connection = markRaw(stream.useChannel('queueStats'))
|
||||
const connection = markRaw(stream.useChannel('queueStats'));
|
||||
|
||||
function clear() {
|
||||
os.confirm({
|
||||
|
|
@ -41,7 +41,7 @@ onMounted(() => {
|
|||
length: 200
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default defineComponent({
|
|||
computed: {
|
||||
name(): string {
|
||||
const el = document.createElement('div');
|
||||
el.textContent = this.app.name
|
||||
el.textContent = this.app.name;
|
||||
return el.innerHTML;
|
||||
},
|
||||
app(): any {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
|||
tags: emojiTags,
|
||||
selectedTags: new Set(),
|
||||
searchEmojis: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function getStatus(instance) {
|
|||
if (instance.isSuspended) return 'suspended';
|
||||
if (instance.isNotResponding) return 'error';
|
||||
return 'alive';
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default defineComponent({
|
|||
description: null,
|
||||
title: null,
|
||||
isSensitive: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -123,11 +123,11 @@ export default defineComponent({
|
|||
os.popupMenu([{
|
||||
text: this.$ts.messagingWithUser,
|
||||
icon: 'fas fa-user',
|
||||
action: () => { this.startUser() }
|
||||
action: () => { this.startUser(); }
|
||||
}, {
|
||||
text: this.$ts.messagingWithGroup,
|
||||
icon: 'fas fa-users',
|
||||
action: () => { this.startGroup() }
|
||||
action: () => { this.startGroup(); }
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export default defineComponent({
|
|||
text: this.text,
|
||||
file: this.file
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localStorage.setItem('message_drafts', JSON.stringify(drafts));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ export default defineComponent({
|
|||
preview_rainbow: `$[rainbow 🍮] $[rainbow.speed=5s 🍮]`,
|
||||
preview_sparkle: `$[sparkle 🍮]`,
|
||||
preview_rotate: `$[rotate 🍮]`,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ defineExpose({
|
|||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)'
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export default defineComponent({
|
|||
readonly: this.readonly,
|
||||
getScriptBlockList: this.getScriptBlockList,
|
||||
getPageBlockList: this.getPageBlockList
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ async function run() {
|
|||
text: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ function registerKey() {
|
|||
registration.value = null;
|
||||
key.lastUsed = new Date();
|
||||
os.success();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterKey(key) {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const init = async () => {
|
|||
accounts.value = response;
|
||||
console.log(accounts.value);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function menu(account, ev) {
|
||||
os.popupMenu([{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const pagination = {
|
|||
params: {
|
||||
sort: '+lastUsedAt'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function revoke(token) {
|
||||
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, reactive, watch } from 'vue';
|
||||
import { reactive, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
|
|
@ -132,8 +132,21 @@ function save() {
|
|||
|
||||
function changeAvatar(ev) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
|
||||
let originalOrCropped = file;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
originalOrCropped = await os.cropImage(file, {
|
||||
aspectRatio: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
avatarId: file.id,
|
||||
avatarId: originalOrCropped.id,
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
|
|
@ -142,8 +155,21 @@ function changeAvatar(ev) {
|
|||
|
||||
function changeBanner(ev) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
|
||||
let originalOrCropped = file;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
originalOrCropped = await os.cropImage(file, {
|
||||
aspectRatio: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
bannerId: file.id,
|
||||
bannerId: originalOrCropped.id,
|
||||
});
|
||||
$i.bannerId = i.bannerId;
|
||||
$i.bannerUrl = i.bannerUrl;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ const darkThemeId = computed({
|
|||
return darkTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
|
||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
|
||||
}
|
||||
});
|
||||
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||
|
|
@ -129,7 +129,7 @@ const lightThemeId = computed({
|
|||
return lightTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
|
||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
|
||||
}
|
||||
});
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ async function save() {
|
|||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const line = lines[i];
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default defineComponent({
|
|||
localOnly: null as boolean | null,
|
||||
files: [] as Misskey.entities.DriveFile[],
|
||||
visibleUsers: [] as Misskey.entities.User[],
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
import { watch } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { v4 as uuid} from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MkTimelinePage',
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default defineComponent({
|
|||
password: '',
|
||||
submitting: false,
|
||||
host,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default defineComponent({
|
|||
return {
|
||||
notes: [],
|
||||
isScrolling: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const defaultRoutes = [
|
|||
{ path: '/gallery', component: page(() => import('./pages/gallery/index.vue')) },
|
||||
{ path: '/gallery/new', component: page(() => import('./pages/gallery/edit.vue')) },
|
||||
{ path: '/gallery/:postId/edit', component: page(() => import('./pages/gallery/edit.vue')), props: route => ({ postId: route.params.postId }) },
|
||||
{ path: '/gallery/:postId', component: page(() => import('./pages/gallery/edit.vue')), props: route => ({ postId: route.params.postId }) },
|
||||
{ path: '/gallery/:postId', component: page(() => import('./pages/gallery/post.vue')), props: route => ({ postId: route.params.postId }) },
|
||||
{ path: '/channels', component: page('channels') },
|
||||
{ path: '/channels/new', component: page('channel-editor') },
|
||||
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const defaultLocaleStringFormats: {[index: string]: string} = {
|
|||
function formatLocaleString(date: Date, format: string): string {
|
||||
return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
|
||||
if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
|
||||
return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
|
||||
return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] });
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
|
|
@ -24,8 +24,8 @@ export function formatDateTimeString(date: Date, format: string): string {
|
|||
return format
|
||||
.replace(/yyyy/g, date.getFullYear().toString())
|
||||
.replace(/yy/g, date.getFullYear().toString().slice(-2))
|
||||
.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
|
||||
.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
|
||||
.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' }))
|
||||
.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' }))
|
||||
.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
|
||||
.replace(/M/g, (date.getMonth() + 1).toString())
|
||||
.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function getNoteMenu(props: {
|
|||
props.note.poll == null
|
||||
);
|
||||
|
||||
let appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
||||
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
||||
|
||||
function del(): void {
|
||||
os.confirm({
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export function getUserMenu(user) {
|
|||
userId: user.id
|
||||
}).then(() => {
|
||||
user.isFollowed = !user.isFollowed;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let menu = [{
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class Hpml {
|
|||
if (this.opts.enableAiScript) {
|
||||
this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
|
||||
storageKey: 'pages:' + this.page.id
|
||||
}), ...initAiLib(this)}, {
|
||||
}), ...initAiLib(this) }, {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ export function physics(container: HTMLElement) {
|
|||
|
||||
const groundThickness = 1024;
|
||||
const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, {
|
||||
isStatic: true,
|
||||
restitution: 0.1,
|
||||
friction: 2
|
||||
isStatic: true,
|
||||
restitution: 0.1,
|
||||
friction: 2
|
||||
});
|
||||
|
||||
//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { v4 as uuid} from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { themeProps, Theme } from './theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const getBuiltinThemesRef = () => {
|
|||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => builtinThemes.value = themes);
|
||||
return builtinThemes;
|
||||
}
|
||||
};
|
||||
|
||||
let timeout = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ type Plugin = {
|
|||
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
|
||||
*/
|
||||
import lightTheme from '@/themes/l-light.json5';
|
||||
import darkTheme from '@/themes/d-dark.json5'
|
||||
import darkTheme from '@/themes/d-dark.json5';
|
||||
|
||||
export class ColdDeviceStorage {
|
||||
public static default = {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default defineComponent({
|
|||
otherMenuItemIndicated,
|
||||
post: os.post,
|
||||
search,
|
||||
openAccountMenu:(ev) => {
|
||||
openAccountMenu: (ev) => {
|
||||
openAccountMenu({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default defineComponent({
|
|||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
openAccountMenu:(ev) => {
|
||||
openAccountMenu: (ev) => {
|
||||
openAccountMenu({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ onBeforeUnmount(() => {
|
|||
os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
|
||||
});
|
||||
|
||||
|
||||
function onOtherDragStart() {
|
||||
dropready = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { throttle } from 'throttle-debounce';
|
||||
import { markRaw } from 'vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import { Storage } from '../../pizzax';
|
||||
import { i18n } from '@/i18n';
|
||||
import { api } from '@/os';
|
||||
import { markRaw } from 'vue';
|
||||
import { Storage } from '../../pizzax';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
|
||||
type ColumnWidget = {
|
||||
name: string;
|
||||
|
|
@ -32,35 +32,35 @@ function copy<T>(x: T): T {
|
|||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
where: 'deviceAccount',
|
||||
default: 'default'
|
||||
default: 'default',
|
||||
},
|
||||
columns: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as Column[]
|
||||
default: [] as Column[],
|
||||
},
|
||||
layout: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as Column['id'][][]
|
||||
default: [] as Column['id'][][],
|
||||
},
|
||||
columnAlign: {
|
||||
where: 'deviceAccount',
|
||||
default: 'left' as 'left' | 'right' | 'center'
|
||||
default: 'left' as 'left' | 'right' | 'center',
|
||||
},
|
||||
alwaysShowMainColumn: {
|
||||
where: 'deviceAccount',
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
navWindow: {
|
||||
where: 'deviceAccount',
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
columnMargin: {
|
||||
where: 'deviceAccount',
|
||||
default: 16
|
||||
default: 16,
|
||||
},
|
||||
columnHeaderHeight: {
|
||||
where: 'deviceAccount',
|
||||
default: 42
|
||||
default: 42,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ export const saveDeck = throttle(1000, () => {
|
|||
value: {
|
||||
columns: deckStore.reactiveState.columns.value,
|
||||
layout: deckStore.reactiveState.layout.value,
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
|||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, WidgetData: any) {
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XWidgets from '@/components/widgets.vue';
|
||||
import XColumn from './column.vue';
|
||||
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||
import XWidgets from '@/components/widgets.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
|
|
|
|||
|
|
@ -103,19 +103,19 @@ const choose = async (ev) => {
|
|||
os.popupMenu([{
|
||||
text: i18n.ts._timelines.home,
|
||||
icon: 'fas fa-home',
|
||||
action: () => { setSrc('home') }
|
||||
action: () => { setSrc('home'); }
|
||||
}, {
|
||||
text: i18n.ts._timelines.local,
|
||||
icon: 'fas fa-comments',
|
||||
action: () => { setSrc('local') }
|
||||
action: () => { setSrc('local'); }
|
||||
}, {
|
||||
text: i18n.ts._timelines.social,
|
||||
icon: 'fas fa-share-alt',
|
||||
action: () => { setSrc('social') }
|
||||
action: () => { setSrc('social'); }
|
||||
}, {
|
||||
text: i18n.ts._timelines.global,
|
||||
icon: 'fas fa-globe',
|
||||
action: () => { setSrc('global') }
|
||||
action: () => { setSrc('global'); }
|
||||
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
|
||||
menuOpened.value = false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
|||
}, { deep: true, immediate: true, });
|
||||
|
||||
const save = throttle(3000, () => {
|
||||
emit('updateProps', widgetProps)
|
||||
emit('updateProps', widgetProps);
|
||||
});
|
||||
|
||||
const configure = async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue