refactor: Composition APIへ移行 (#8138)
* components/drive-file-thumbnail.vue * components/drive-select-dialog.vue * components/drive-window.vue * wip * wip drive.file.vue, drive.vue * fix prop * wip( * components/drive.folder.vue * maybe ok * ✌️ * fix variable * FIX FOLDER VARIABLE * components/emoji-picker-dialog.vue * Hate `$emit` * hate global property * components/emoji-picker-window.vue * components/emoji-picker.section.vue * fix * fixx * wip components/emoji-picker.vue * fix * defineExpose * ユニコード絵文字の型をもっといい感じに * components/featured-photos.vue * components/follow-button.vue * forgot-password.vue * forgot-password.vue * 🎨 * fix
This commit is contained in:
parent
efb0ffc4ec
commit
7be09a4af9
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')">
|
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||||
<div class="mk-dialog">
|
<div class="mk-dialog">
|
||||||
<div v-if="icon" class="icon">
|
<div v-if="icon" class="icon">
|
||||||
<i :class="icon"></i>
|
<i :class="icon"></i>
|
||||||
@ -28,8 +28,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
|
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
|
||||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
|
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
|
||||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton>
|
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actions" class="buttons">
|
<div v-if="actions" class="buttons">
|
||||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
|
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
|
||||||
@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue';
|
|||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/form/select.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
type: HTMLInputElement['type'];
|
type: HTMLInputElement['type'];
|
||||||
|
@ -14,42 +14,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||||
import { ColdDeviceStorage } from '@/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
file: Misskey.entities.DriveFile;
|
||||||
ImgWithBlurhash
|
fit: string;
|
||||||
},
|
}>();
|
||||||
props: {
|
|
||||||
file: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
fit: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'cover'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isContextmenuShowing: false,
|
|
||||||
isDragging: false,
|
|
||||||
|
|
||||||
};
|
const is = computed(() => {
|
||||||
},
|
if (props.file.type.startsWith('image/')) return 'image';
|
||||||
computed: {
|
if (props.file.type.startsWith('video/')) return 'video';
|
||||||
is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
|
if (props.file.type === 'audio/midi') return 'midi';
|
||||||
if (this.file.type.startsWith('image/')) return 'image';
|
if (props.file.type.startsWith('audio/')) return 'audio';
|
||||||
if (this.file.type.startsWith('video/')) return 'video';
|
if (props.file.type.endsWith('/csv')) return 'csv';
|
||||||
if (this.file.type === 'audio/midi') return 'midi';
|
if (props.file.type.endsWith('/pdf')) return 'pdf';
|
||||||
if (this.file.type.startsWith('audio/')) return 'audio';
|
if (props.file.type.startsWith('text/')) return 'textfile';
|
||||||
if (this.file.type.endsWith('/csv')) return 'csv';
|
|
||||||
if (this.file.type.endsWith('/pdf')) return 'pdf';
|
|
||||||
if (this.file.type.startsWith('text/')) return 'textfile';
|
|
||||||
if ([
|
if ([
|
||||||
"application/zip",
|
"application/zip",
|
||||||
"application/x-cpio",
|
"application/x-cpio",
|
||||||
@ -60,25 +42,14 @@ export default defineComponent({
|
|||||||
"application/x-tar",
|
"application/x-tar",
|
||||||
"application/gzip",
|
"application/gzip",
|
||||||
"application/x-7z-compressed"
|
"application/x-7z-compressed"
|
||||||
].some(e => e === this.file.type)) return 'archive';
|
].some(e => e === props.file.type)) return 'archive';
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
},
|
});
|
||||||
isThumbnailAvailable(): boolean {
|
|
||||||
return this.file.thumbnailUrl
|
const isThumbnailAvailable = computed(() => {
|
||||||
? (this.is === 'image' || this.is === 'video')
|
return props.file.thumbnailUrl
|
||||||
|
? (is.value === 'image' as const || is.value === 'video')
|
||||||
: false;
|
: false;
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
|
||||||
if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
volumechange() {
|
|
||||||
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
|
|
||||||
ColdDeviceStorage.set('mediaVolume', audioTag.volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -7,64 +7,51 @@
|
|||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
|
{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
|
||||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||||
</template>
|
</template>
|
||||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import XDrive from './drive.vue';
|
import XDrive from './drive.vue';
|
||||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
withDefaults(defineProps<{
|
||||||
components: {
|
type?: 'file' | 'folder';
|
||||||
XDrive,
|
multiple: boolean;
|
||||||
XModalWindow,
|
}>(), {
|
||||||
},
|
type: 'file',
|
||||||
|
|
||||||
props: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'file'
|
|
||||||
},
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['done', 'closed'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selected: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
ok() {
|
|
||||||
this.$emit('done', this.selected);
|
|
||||||
this.$refs.dialog.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.$emit('done');
|
|
||||||
this.$refs.dialog.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeSelection(xs) {
|
|
||||||
this.selected = xs;
|
|
||||||
},
|
|
||||||
|
|
||||||
number
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'done', r?: Misskey.entities.DriveFile[]): void;
|
||||||
|
(e: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
|
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
|
|
||||||
|
function ok() {
|
||||||
|
emit('done', selected.value);
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('done');
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSelection(files: Misskey.entities.DriveFile[]) {
|
||||||
|
selected.value = files;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,42 +3,27 @@
|
|||||||
:initial-width="800"
|
:initial-width="800"
|
||||||
:initial-height="500"
|
:initial-height="500"
|
||||||
:can-resize="true"
|
:can-resize="true"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ $ts.drive }}
|
{{ i18n.locale.drive }}
|
||||||
</template>
|
</template>
|
||||||
<XDrive :initial-folder="initialFolder"/>
|
<XDrive :initial-folder="initialFolder"/>
|
||||||
</XWindow>
|
</XWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import XDrive from './drive.vue';
|
import XDrive from './drive.vue';
|
||||||
import XWindow from '@/components/ui/window.vue';
|
import XWindow from '@/components/ui/window.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps<{
|
||||||
components: {
|
initialFolder?: Misskey.entities.DriveFolder;
|
||||||
XDrive,
|
}>();
|
||||||
XWindow,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
initialFolder: {
|
(e: 'closed'): void;
|
||||||
type: Object,
|
}>();
|
||||||
required: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['closed'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,17 +8,17 @@
|
|||||||
@dragstart="onDragstart"
|
@dragstart="onDragstart"
|
||||||
@dragend="onDragend"
|
@dragend="onDragend"
|
||||||
>
|
>
|
||||||
<div v-if="$i.avatarId == file.id" class="label">
|
<div v-if="$i?.avatarId == file.id" class="label">
|
||||||
<img src="/client-assets/label.svg"/>
|
<img src="/client-assets/label.svg"/>
|
||||||
<p>{{ $ts.avatar }}</p>
|
<p>{{ i18n.locale.avatar }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$i.bannerId == file.id" class="label">
|
<div v-if="$i?.bannerId == file.id" class="label">
|
||||||
<img src="/client-assets/label.svg"/>
|
<img src="/client-assets/label.svg"/>
|
||||||
<p>{{ $ts.banner }}</p>
|
<p>{{ i18n.locale.banner }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="file.isSensitive" class="label red">
|
<div v-if="file.isSensitive" class="label red">
|
||||||
<img src="/client-assets/label-red.svg"/>
|
<img src="/client-assets/label-red.svg"/>
|
||||||
<p>{{ $ts.nsfw }}</p>
|
<p>{{ i18n.locale.nsfw }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||||
@ -30,179 +30,155 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from '@/filters/bytes';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
components: {
|
file: Misskey.entities.DriveFile;
|
||||||
MkDriveFileThumbnail
|
isSelected?: boolean;
|
||||||
},
|
selectMode?: boolean;
|
||||||
|
}>(), {
|
||||||
|
isSelected: false,
|
||||||
|
selectMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
file: {
|
(e: 'chosen', r: Misskey.entities.DriveFile): void;
|
||||||
type: Object,
|
(e: 'dragstart'): void;
|
||||||
required: true,
|
(e: 'dragend'): void;
|
||||||
},
|
}>();
|
||||||
isSelected: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
selectMode: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['chosen'],
|
const isDragging = ref(false);
|
||||||
|
|
||||||
data() {
|
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
|
||||||
return {
|
|
||||||
isDragging: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
function getMenu() {
|
||||||
// TODO: parentへの参照を無くす
|
|
||||||
browser(): any {
|
|
||||||
return this.$parent;
|
|
||||||
},
|
|
||||||
title(): string {
|
|
||||||
return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getMenu() {
|
|
||||||
return [{
|
return [{
|
||||||
text: this.$ts.rename,
|
text: i18n.locale.rename,
|
||||||
icon: 'fas fa-i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
action: this.rename
|
action: rename
|
||||||
}, {
|
}, {
|
||||||
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
|
text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
|
||||||
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
|
icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||||
action: this.toggleSensitive
|
action: toggleSensitive
|
||||||
}, {
|
}, {
|
||||||
text: this.$ts.describeFile,
|
text: i18n.locale.describeFile,
|
||||||
icon: 'fas fa-i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
action: this.describe
|
action: describe
|
||||||
}, null, {
|
}, null, {
|
||||||
text: this.$ts.copyUrl,
|
text: i18n.locale.copyUrl,
|
||||||
icon: 'fas fa-link',
|
icon: 'fas fa-link',
|
||||||
action: this.copyUrl
|
action: copyUrl
|
||||||
}, {
|
}, {
|
||||||
type: 'a',
|
type: 'a',
|
||||||
href: this.file.url,
|
href: props.file.url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
text: this.$ts.download,
|
text: i18n.locale.download,
|
||||||
icon: 'fas fa-download',
|
icon: 'fas fa-download',
|
||||||
download: this.file.name
|
download: props.file.name
|
||||||
}, null, {
|
}, null, {
|
||||||
text: this.$ts.delete,
|
text: i18n.locale.delete,
|
||||||
icon: 'fas fa-trash-alt',
|
icon: 'fas fa-trash-alt',
|
||||||
danger: true,
|
danger: true,
|
||||||
action: this.deleteFile
|
action: deleteFile
|
||||||
}];
|
}];
|
||||||
},
|
}
|
||||||
|
|
||||||
onClick(ev) {
|
function onClick(ev: MouseEvent) {
|
||||||
if (this.selectMode) {
|
if (props.selectMode) {
|
||||||
this.$emit('chosen', this.file);
|
emit('chosen', props.file);
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
|
os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onContextmenu(e) {
|
function onContextmenu(e: MouseEvent) {
|
||||||
os.contextMenu(this.getMenu(), e);
|
os.contextMenu(getMenu(), e);
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragstart(e) {
|
function onDragstart(e: DragEvent) {
|
||||||
|
if (e.dataTransfer) {
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
|
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
|
||||||
this.isDragging = true;
|
}
|
||||||
|
isDragging.value = true;
|
||||||
|
|
||||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
emit('dragstart');
|
||||||
// (=あなたの子供が、ドラッグを開始しましたよ)
|
}
|
||||||
this.browser.isDragSource = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
onDragend(e) {
|
function onDragend() {
|
||||||
this.isDragging = false;
|
isDragging.value = false;
|
||||||
this.browser.isDragSource = false;
|
emit('dragend');
|
||||||
},
|
}
|
||||||
|
|
||||||
rename() {
|
function rename() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: this.$ts.renameFile,
|
title: i18n.locale.renameFile,
|
||||||
placeholder: this.$ts.inputNewFileName,
|
placeholder: i18n.locale.inputNewFileName,
|
||||||
default: this.file.name,
|
default: props.file.name,
|
||||||
allowEmpty: false
|
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: this.file.id,
|
fileId: props.file.id,
|
||||||
name: name
|
name: name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
describe() {
|
function describe() {
|
||||||
os.popup(import('@/components/media-caption.vue'), {
|
os.popup(import('@/components/media-caption.vue'), {
|
||||||
title: this.$ts.describeFile,
|
title: i18n.locale.describeFile,
|
||||||
input: {
|
input: {
|
||||||
placeholder: this.$ts.inputNewDescription,
|
placeholder: i18n.locale.inputNewDescription,
|
||||||
default: this.file.comment !== null ? this.file.comment : '',
|
default: props.file.comment !== null ? props.file.comment : '',
|
||||||
},
|
},
|
||||||
image: this.file
|
image: props.file
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (!result || result.canceled) return;
|
if (!result || result.canceled) return;
|
||||||
let comment = result.result;
|
let comment = result.result;
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: this.file.id,
|
fileId: props.file.id,
|
||||||
comment: comment.length == 0 ? null : comment
|
comment: comment.length == 0 ? null : comment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
},
|
}
|
||||||
|
|
||||||
toggleSensitive() {
|
function toggleSensitive() {
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: this.file.id,
|
fileId: props.file.id,
|
||||||
isSensitive: !this.file.isSensitive
|
isSensitive: !props.file.isSensitive
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
copyUrl() {
|
function copyUrl() {
|
||||||
copyToClipboard(this.file.url);
|
copyToClipboard(props.file.url);
|
||||||
os.success();
|
os.success();
|
||||||
},
|
}
|
||||||
|
/*
|
||||||
addApp() {
|
function addApp() {
|
||||||
alert('not implemented yet');
|
alert('not implemented yet');
|
||||||
},
|
}
|
||||||
|
*/
|
||||||
async deleteFile() {
|
async function deleteFile() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
|
text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.api('drive/files/delete', {
|
os.api('drive/files/delete', {
|
||||||
fileId: this.file.id
|
fileId: props.file.id
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
bytes
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -19,74 +19,66 @@
|
|||||||
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
|
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
|
||||||
{{ folder.name }}
|
{{ folder.name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="$store.state.uploadFolder == folder.id" class="upload">
|
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
||||||
{{ $ts.uploadFolder }}
|
{{ i18n.locale.uploadFolder }}
|
||||||
</p>
|
</p>
|
||||||
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
props: {
|
folder: Misskey.entities.DriveFolder;
|
||||||
folder: {
|
isSelected?: boolean;
|
||||||
type: Object,
|
selectMode?: boolean;
|
||||||
required: true,
|
}>(), {
|
||||||
},
|
isSelected: false,
|
||||||
isSelected: {
|
selectMode: false,
|
||||||
type: Boolean,
|
});
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
selectMode: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['chosen'],
|
const emit = defineEmits<{
|
||||||
|
(e: 'chosen', v: Misskey.entities.DriveFolder): void;
|
||||||
|
(e: 'move', v: Misskey.entities.DriveFolder): void;
|
||||||
|
(e: 'upload', file: File, folder: Misskey.entities.DriveFolder);
|
||||||
|
(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
||||||
|
(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
||||||
|
(e: 'dragstart'): void;
|
||||||
|
(e: 'dragend'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
data() {
|
const hover = ref(false);
|
||||||
return {
|
const draghover = ref(false);
|
||||||
hover: false,
|
const isDragging = ref(false);
|
||||||
draghover: false,
|
|
||||||
isDragging: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const title = computed(() => props.folder.name);
|
||||||
browser(): any {
|
|
||||||
return this.$parent;
|
|
||||||
},
|
|
||||||
title(): string {
|
|
||||||
return this.folder.name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function checkboxClicked(e) {
|
||||||
checkboxClicked(e) {
|
emit('chosen', props.folder);
|
||||||
this.$emit('chosen', this.folder);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onClick() {
|
function onClick() {
|
||||||
this.browser.move(this.folder);
|
emit('move', props.folder);
|
||||||
},
|
}
|
||||||
|
|
||||||
onMouseover() {
|
function onMouseover() {
|
||||||
this.hover = true;
|
hover.value = true;
|
||||||
},
|
}
|
||||||
|
|
||||||
onMouseout() {
|
function onMouseout() {
|
||||||
this.hover = false
|
hover.value = false
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDragover(e: DragEvent) {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDragover(e) {
|
|
||||||
// 自分自身がドラッグされている場合
|
// 自分自身がドラッグされている場合
|
||||||
if (this.isDragging) {
|
if (isDragging.value) {
|
||||||
// 自分自身にはドロップさせない
|
// 自分自身にはドロップさせない
|
||||||
e.dataTransfer.dropEffect = 'none';
|
e.dataTransfer.dropEffect = 'none';
|
||||||
return;
|
return;
|
||||||
@ -101,23 +93,25 @@ export default defineComponent({
|
|||||||
} else {
|
} else {
|
||||||
e.dataTransfer.dropEffect = 'none';
|
e.dataTransfer.dropEffect = 'none';
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragenter() {
|
function onDragenter() {
|
||||||
if (!this.isDragging) this.draghover = true;
|
if (!isDragging.value) draghover.value = true;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragleave() {
|
function onDragleave() {
|
||||||
this.draghover = false;
|
draghover.value = false;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDrop(e) {
|
function onDrop(e: DragEvent) {
|
||||||
this.draghover = false;
|
draghover.value = false;
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
for (const file of Array.from(e.dataTransfer.files)) {
|
for (const file of Array.from(e.dataTransfer.files)) {
|
||||||
this.browser.upload(file, this.folder);
|
emit('upload', file, props.folder);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -126,10 +120,10 @@ export default defineComponent({
|
|||||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile != '') {
|
if (driveFile != null && driveFile != '') {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
this.browser.removeFile(file.id);
|
emit('removeFile', file.id);
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: this.folder.id
|
folderId: props.folder.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -140,122 +134,118 @@ export default defineComponent({
|
|||||||
const folder = JSON.parse(driveFolder);
|
const folder = JSON.parse(driveFolder);
|
||||||
|
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (folder.id == this.folder.id) return;
|
if (folder.id == props.folder.id) return;
|
||||||
|
|
||||||
this.browser.removeFolder(folder.id);
|
emit('removeFolder', folder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
parentId: this.folder.id
|
parentId: props.folder.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// noop
|
// noop
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case 'detected-circular-definition':
|
case 'detected-circular-definition':
|
||||||
os.alert({
|
os.alert({
|
||||||
title: this.$ts.unableToProcess,
|
title: i18n.locale.unableToProcess,
|
||||||
text: this.$ts.circularReferenceFolder
|
text: i18n.locale.circularReferenceFolder
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.somethingHappened
|
text: i18n.locale.somethingHappened
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDragstart(e: DragEvent) {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDragstart(e) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
|
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
|
||||||
this.isDragging = true;
|
isDragging.value = true;
|
||||||
|
|
||||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
||||||
// (=あなたの子供が、ドラッグを開始しましたよ)
|
// (=あなたの子供が、ドラッグを開始しましたよ)
|
||||||
this.browser.isDragSource = true;
|
emit('dragstart');
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragend() {
|
function onDragend() {
|
||||||
this.isDragging = false;
|
isDragging.value = false;
|
||||||
this.browser.isDragSource = false;
|
emit('dragend');
|
||||||
},
|
}
|
||||||
|
|
||||||
go() {
|
function go() {
|
||||||
this.browser.move(this.folder.id);
|
emit('move', props.folder.id);
|
||||||
},
|
}
|
||||||
|
|
||||||
newWindow() {
|
function rename() {
|
||||||
this.browser.newWindow(this.folder);
|
|
||||||
},
|
|
||||||
|
|
||||||
rename() {
|
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: this.$ts.renameFolder,
|
title: i18n.locale.renameFolder,
|
||||||
placeholder: this.$ts.inputNewFolderName,
|
placeholder: i18n.locale.inputNewFolderName,
|
||||||
default: this.folder.name
|
default: props.folder.name
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: this.folder.id,
|
folderId: props.folder.id,
|
||||||
name: name
|
name: name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
deleteFolder() {
|
function deleteFolder() {
|
||||||
os.api('drive/folders/delete', {
|
os.api('drive/folders/delete', {
|
||||||
folderId: this.folder.id
|
folderId: props.folder.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (this.$store.state.uploadFolder === this.folder.id) {
|
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||||
this.$store.set('uploadFolder', null);
|
defaultStore.set('uploadFolder', null);
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
switch(err.id) {
|
switch(err.id) {
|
||||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: this.$ts.unableToDelete,
|
title: i18n.locale.unableToDelete,
|
||||||
text: this.$ts.hasChildFilesOrFolders
|
text: i18n.locale.hasChildFilesOrFolders
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.unableToDelete
|
text: i18n.locale.unableToDelete
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
setAsUploadFolder() {
|
function setAsUploadFolder() {
|
||||||
this.$store.set('uploadFolder', this.folder.id);
|
defaultStore.set('uploadFolder', props.folder.id);
|
||||||
},
|
}
|
||||||
|
|
||||||
onContextmenu(e) {
|
function onContextmenu(e) {
|
||||||
os.contextMenu([{
|
os.contextMenu([{
|
||||||
text: this.$ts.openInWindow,
|
text: i18n.locale.openInWindow,
|
||||||
icon: 'fas fa-window-restore',
|
icon: 'fas fa-window-restore',
|
||||||
action: () => {
|
action: () => {
|
||||||
os.popup(import('./drive-window.vue'), {
|
os.popup(import('./drive-window.vue'), {
|
||||||
initialFolder: this.folder
|
initialFolder: props.folder
|
||||||
}, {
|
}, {
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
}
|
}
|
||||||
}, null, {
|
}, null, {
|
||||||
text: this.$ts.rename,
|
text: i18n.locale.rename,
|
||||||
icon: 'fas fa-i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
action: this.rename
|
action: rename,
|
||||||
}, null, {
|
}, null, {
|
||||||
text: this.$ts.delete,
|
text: i18n.locale.delete,
|
||||||
icon: 'fas fa-trash-alt',
|
icon: 'fas fa-trash-alt',
|
||||||
danger: true,
|
danger: true,
|
||||||
action: this.deleteFolder
|
action: deleteFolder,
|
||||||
}], e);
|
}], e);
|
||||||
},
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -8,51 +8,48 @@
|
|||||||
@drop.stop="onDrop"
|
@drop.stop="onDrop"
|
||||||
>
|
>
|
||||||
<i v-if="folder == null" class="fas fa-cloud"></i>
|
<i v-if="folder == null" class="fas fa-cloud"></i>
|
||||||
<span>{{ folder == null ? $ts.drive : folder.name }}</span>
|
<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
folder?: Misskey.entities.DriveFolder;
|
||||||
folder: {
|
parentFolder: Misskey.entities.DriveFolder | null;
|
||||||
type: Object,
|
}>();
|
||||||
required: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const emit = defineEmits<{
|
||||||
return {
|
(e: 'move', v?: Misskey.entities.DriveFolder): void;
|
||||||
hover: false,
|
(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
|
||||||
draghover: false,
|
(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
||||||
};
|
(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
||||||
},
|
}>();
|
||||||
|
|
||||||
computed: {
|
const hover = ref(false);
|
||||||
browser(): any {
|
const draghover = ref(false);
|
||||||
return this.$parent;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function onClick() {
|
||||||
onClick() {
|
emit('move', props.folder);
|
||||||
this.browser.move(this.folder);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onMouseover() {
|
function onMouseover() {
|
||||||
this.hover = true;
|
hover.value = true;
|
||||||
},
|
}
|
||||||
|
|
||||||
onMouseout() {
|
function onMouseout() {
|
||||||
this.hover = false;
|
hover.value = false;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDragover(e: DragEvent) {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDragover(e) {
|
|
||||||
// このフォルダがルートかつカレントディレクトリならドロップ禁止
|
// このフォルダがルートかつカレントディレクトリならドロップ禁止
|
||||||
if (this.folder == null && this.browser.folder == null) {
|
if (props.folder == null && props.parentFolder == null) {
|
||||||
e.dataTransfer.dropEffect = 'none';
|
e.dataTransfer.dropEffect = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,23 +64,25 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragenter() {
|
function onDragenter() {
|
||||||
if (this.folder || this.browser.folder) this.draghover = true;
|
if (props.folder || props.parentFolder) draghover.value = true;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragleave() {
|
function onDragleave() {
|
||||||
if (this.folder || this.browser.folder) this.draghover = false;
|
if (props.folder || props.parentFolder) draghover.value = false;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDrop(e) {
|
function onDrop(e: DragEvent) {
|
||||||
this.draghover = false;
|
draghover.value = false;
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
for (const file of Array.from(e.dataTransfer.files)) {
|
for (const file of Array.from(e.dataTransfer.files)) {
|
||||||
this.browser.upload(file, this.folder);
|
emit('upload', file, props.folder);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -92,10 +91,10 @@ export default defineComponent({
|
|||||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile != '') {
|
if (driveFile != null && driveFile != '') {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
this.browser.removeFile(file.id);
|
emit('removeFile', file.id);
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: this.folder ? this.folder.id : null
|
folderId: props.folder ? props.folder.id : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -105,17 +104,15 @@ export default defineComponent({
|
|||||||
if (driveFolder != null && driveFolder != '') {
|
if (driveFolder != null && driveFolder != '') {
|
||||||
const folder = JSON.parse(driveFolder);
|
const folder = JSON.parse(driveFolder);
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (this.folder && folder.id == this.folder.id) return;
|
if (props.folder && folder.id == props.folder.id) return;
|
||||||
this.browser.removeFolder(folder.id);
|
emit('removeFolder', folder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
parentId: this.folder ? this.folder.id : null
|
parentId: props.folder ? props.folder.id : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -2,10 +2,24 @@
|
|||||||
<div class="yfudmmck">
|
<div class="yfudmmck">
|
||||||
<nav>
|
<nav>
|
||||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||||
<XNavFolder :class="{ current: folder == null }"/>
|
<XNavFolder
|
||||||
|
:class="{ current: folder == null }"
|
||||||
|
:parent-folder="folder"
|
||||||
|
@move="move"
|
||||||
|
@upload="upload"
|
||||||
|
@removeFile="removeFile"
|
||||||
|
@removeFolder="removeFolder"
|
||||||
|
/>
|
||||||
<template v-for="f in hierarchyFolders">
|
<template v-for="f in hierarchyFolders">
|
||||||
<span class="separator"><i class="fas fa-angle-right"></i></span>
|
<span class="separator"><i class="fas fa-angle-right"></i></span>
|
||||||
<XNavFolder :folder="f"/>
|
<XNavFolder
|
||||||
|
:folder="f"
|
||||||
|
:parent-folder="folder"
|
||||||
|
@move="move"
|
||||||
|
@upload="upload"
|
||||||
|
@removeFile="removeFile"
|
||||||
|
@removeFolder="removeFolder"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
|
<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
|
||||||
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
|
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
|
||||||
@ -22,192 +36,154 @@
|
|||||||
>
|
>
|
||||||
<div ref="contents" class="contents">
|
<div ref="contents" class="contents">
|
||||||
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
||||||
<XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
|
<XFolder
|
||||||
|
v-for="(f, i) in folders"
|
||||||
|
:key="f.id"
|
||||||
|
v-anim="i"
|
||||||
|
class="folder"
|
||||||
|
:folder="f"
|
||||||
|
:select-mode="select === 'folder'"
|
||||||
|
:is-selected="selectedFolders.some(x => x.id === f.id)"
|
||||||
|
@chosen="chooseFolder"
|
||||||
|
@move="move"
|
||||||
|
@upload="upload"
|
||||||
|
@removeFile="removeFile"
|
||||||
|
@removeFolder="removeFolder"
|
||||||
|
@dragstart="isDragSource = true"
|
||||||
|
@dragend="isDragSource = false"
|
||||||
|
/>
|
||||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||||
<MkButton v-if="moreFolders" ref="moreFolders">{{ $ts.loadMore }}</MkButton>
|
<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="files.length > 0" ref="filesContainer" class="files">
|
<div v-show="files.length > 0" ref="filesContainer" class="files">
|
||||||
<XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
|
<XFile
|
||||||
|
v-for="(file, i) in files"
|
||||||
|
:key="file.id"
|
||||||
|
v-anim="i"
|
||||||
|
class="file"
|
||||||
|
:file="file"
|
||||||
|
:select-mode="select === 'file'"
|
||||||
|
:is-selected="selectedFiles.some(x => x.id === file.id)"
|
||||||
|
@chosen="chooseFile"
|
||||||
|
@dragstart="isDragSource = true"
|
||||||
|
@dragend="isDragSource = false"
|
||||||
|
/>
|
||||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ $ts.loadMore }}</MkButton>
|
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
|
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
|
||||||
<p v-if="draghover">{{ $t('empty-draghover') }}</p>
|
<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
|
||||||
<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
|
<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
|
||||||
<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
|
<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="draghover" class="dropzone"></div>
|
<div v-if="draghover" class="dropzone"></div>
|
||||||
<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
|
<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import XNavFolder from './drive.nav-folder.vue';
|
import XNavFolder from './drive.nav-folder.vue';
|
||||||
import XFolder from './drive.folder.vue';
|
import XFolder from './drive.folder.vue';
|
||||||
import XFile from './drive.file.vue';
|
import XFile from './drive.file.vue';
|
||||||
import MkButton from './ui/button.vue';
|
import MkButton from './ui/button.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
components: {
|
initialFolder?: Misskey.entities.DriveFolder;
|
||||||
XNavFolder,
|
type?: string;
|
||||||
XFolder,
|
multiple?: boolean;
|
||||||
XFile,
|
select?: 'file' | 'folder' | null;
|
||||||
MkButton,
|
}>(), {
|
||||||
},
|
multiple: false,
|
||||||
|
select: null,
|
||||||
|
});
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
initialFolder: {
|
(e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
||||||
type: Object,
|
(e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||||
required: false
|
(e: 'move-root'): void;
|
||||||
},
|
(e: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
||||||
type: {
|
(e: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||||
type: String,
|
}>();
|
||||||
required: false,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
|
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
|
||||||
|
const fileInput = ref<HTMLInputElement>();
|
||||||
|
|
||||||
data() {
|
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||||
return {
|
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
/**
|
const folders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||||
* 現在の階層(フォルダ)
|
const moreFiles = ref(false);
|
||||||
* * null でルートを表す
|
const moreFolders = ref(false);
|
||||||
*/
|
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||||
folder: null,
|
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
|
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||||
|
const uploadings = os.uploads;
|
||||||
|
const connection = stream.useChannel('drive');
|
||||||
|
|
||||||
files: [],
|
// ドロップされようとしているか
|
||||||
folders: [],
|
const draghover = ref(false);
|
||||||
moreFiles: false,
|
|
||||||
moreFolders: false,
|
|
||||||
hierarchyFolders: [],
|
|
||||||
selectedFiles: [],
|
|
||||||
selectedFolders: [],
|
|
||||||
uploadings: os.uploads,
|
|
||||||
connection: null,
|
|
||||||
|
|
||||||
/**
|
// 自身の所有するアイテムがドラッグをスタートさせたか
|
||||||
* ドロップされようとしているか
|
// (自分自身の階層にドロップできないようにするためのフラグ)
|
||||||
*/
|
const isDragSource = ref(false);
|
||||||
draghover: false,
|
|
||||||
|
|
||||||
/**
|
const fetching = ref(true);
|
||||||
* 自信の所有するアイテムがドラッグをスタートさせたか
|
|
||||||
* (自分自身の階層にドロップできないようにするためのフラグ)
|
|
||||||
*/
|
|
||||||
isDragSource: false,
|
|
||||||
|
|
||||||
fetching: true,
|
const ilFilesObserver = new IntersectionObserver(
|
||||||
|
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
|
||||||
|
)
|
||||||
|
|
||||||
ilFilesObserver: new IntersectionObserver(
|
watch(folder, () => emit('cd', folder.value));
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting)
|
|
||||||
&& !this.fetching && this.moreFiles &&
|
|
||||||
this.fetchMoreFiles()
|
|
||||||
),
|
|
||||||
moreFilesElement: null as Element,
|
|
||||||
|
|
||||||
};
|
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
|
||||||
},
|
addFile(file, true);
|
||||||
|
}
|
||||||
|
|
||||||
watch: {
|
function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
|
||||||
folder() {
|
const current = folder.value ? folder.value.id : null;
|
||||||
this.$emit('cd', this.folder);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connection = markRaw(stream.useChannel('drive'));
|
|
||||||
|
|
||||||
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
|
|
||||||
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
|
|
||||||
this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
|
|
||||||
this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
|
|
||||||
this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
|
|
||||||
this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
|
|
||||||
|
|
||||||
if (this.initialFolder) {
|
|
||||||
this.move(this.initialFolder);
|
|
||||||
} else {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
if (this.$store.state.enableInfiniteScroll) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
this.connection.dispose();
|
|
||||||
this.ilFilesObserver.disconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onStreamDriveFileCreated(file) {
|
|
||||||
this.addFile(file, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
onStreamDriveFileUpdated(file) {
|
|
||||||
const current = this.folder ? this.folder.id : null;
|
|
||||||
if (current != file.folderId) {
|
if (current != file.folderId) {
|
||||||
this.removeFile(file);
|
removeFile(file);
|
||||||
} else {
|
} else {
|
||||||
this.addFile(file, true);
|
addFile(file, true);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onStreamDriveFileDeleted(fileId) {
|
function onStreamDriveFileDeleted(fileId: string) {
|
||||||
this.removeFile(fileId);
|
removeFile(fileId);
|
||||||
},
|
}
|
||||||
|
|
||||||
onStreamDriveFolderCreated(folder) {
|
function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
|
||||||
this.addFolder(folder, true);
|
addFolder(createdFolder, true);
|
||||||
},
|
}
|
||||||
|
|
||||||
onStreamDriveFolderUpdated(folder) {
|
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
|
||||||
const current = this.folder ? this.folder.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current != folder.parentId) {
|
if (current != updatedFolder.parentId) {
|
||||||
this.removeFolder(folder);
|
removeFolder(updatedFolder);
|
||||||
} else {
|
} else {
|
||||||
this.addFolder(folder, true);
|
addFolder(updatedFolder, true);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onStreamDriveFolderDeleted(folderId) {
|
function onStreamDriveFolderDeleted(folderId: string) {
|
||||||
this.removeFolder(folderId);
|
removeFolder(folderId);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDragover(e: DragEvent): any {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDragover(e): any {
|
|
||||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||||
if (this.isDragSource) {
|
if (isDragSource.value) {
|
||||||
// 自分自身にはドロップさせない
|
// 自分自身にはドロップさせない
|
||||||
e.dataTransfer.dropEffect = 'none';
|
e.dataTransfer.dropEffect = 'none';
|
||||||
return;
|
return;
|
||||||
@ -216,7 +192,6 @@ export default defineComponent({
|
|||||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||||
|
|
||||||
if (isFile || isDriveFile || isDriveFolder) {
|
if (isFile || isDriveFile || isDriveFolder) {
|
||||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||||
} else {
|
} else {
|
||||||
@ -224,23 +199,25 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragenter(e) {
|
function onDragenter() {
|
||||||
if (!this.isDragSource) this.draghover = true;
|
if (!isDragSource.value) draghover.value = true;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragleave(e) {
|
function onDragleave() {
|
||||||
this.draghover = false;
|
draghover.value = false;
|
||||||
},
|
}
|
||||||
|
|
||||||
onDrop(e): any {
|
function onDrop(e: DragEvent): any {
|
||||||
this.draghover = false;
|
draghover.value = false;
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
// ドロップされてきたものがファイルだったら
|
// ドロップされてきたものがファイルだったら
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
for (const file of Array.from(e.dataTransfer.files)) {
|
for (const file of Array.from(e.dataTransfer.files)) {
|
||||||
this.upload(file, this.folder);
|
upload(file, folder.value);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -249,11 +226,11 @@ export default defineComponent({
|
|||||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile != '') {
|
if (driveFile != null && driveFile != '') {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
if (this.files.some(f => f.id == file.id)) return;
|
if (files.value.some(f => f.id == file.id)) return;
|
||||||
this.removeFile(file.id);
|
removeFile(file.id);
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: this.folder ? this.folder.id : null
|
folderId: folder.value ? folder.value.id : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -261,377 +238,398 @@ export default defineComponent({
|
|||||||
//#region ドライブのフォルダ
|
//#region ドライブのフォルダ
|
||||||
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||||
if (driveFolder != null && driveFolder != '') {
|
if (driveFolder != null && driveFolder != '') {
|
||||||
const folder = JSON.parse(driveFolder);
|
const droppedFolder = JSON.parse(driveFolder);
|
||||||
|
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (this.folder && folder.id == this.folder.id) return false;
|
if (folder.value && droppedFolder.id == folder.value.id) return false;
|
||||||
if (this.folders.some(f => f.id == folder.id)) return false;
|
if (folders.value.some(f => f.id == droppedFolder.id)) return false;
|
||||||
this.removeFolder(folder.id);
|
removeFolder(droppedFolder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: folder.id,
|
folderId: droppedFolder.id,
|
||||||
parentId: this.folder ? this.folder.id : null
|
parentId: folder.value ? folder.value.id : null
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// noop
|
// noop
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case 'detected-circular-definition':
|
case 'detected-circular-definition':
|
||||||
os.alert({
|
os.alert({
|
||||||
title: this.$ts.unableToProcess,
|
title: i18n.locale.unableToProcess,
|
||||||
text: this.$ts.circularReferenceFolder
|
text: i18n.locale.circularReferenceFolder
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.somethingHappened
|
text: i18n.locale.somethingHappened
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
}
|
||||||
|
|
||||||
selectLocalFile() {
|
function selectLocalFile() {
|
||||||
(this.$refs.fileInput as any).click();
|
fileInput.value?.click();
|
||||||
},
|
}
|
||||||
|
|
||||||
urlUpload() {
|
function urlUpload() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: this.$ts.uploadFromUrl,
|
title: i18n.locale.uploadFromUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
placeholder: this.$ts.uploadFromUrlDescription
|
placeholder: i18n.locale.uploadFromUrlDescription
|
||||||
}).then(({ canceled, result: url }) => {
|
}).then(({ canceled, result: url }) => {
|
||||||
if (canceled) return;
|
if (canceled || !url) return;
|
||||||
os.api('drive/files/upload-from-url', {
|
os.api('drive/files/upload-from-url', {
|
||||||
url: url,
|
url: url,
|
||||||
folderId: this.folder ? this.folder.id : undefined
|
folderId: folder.value ? folder.value.id : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
os.alert({
|
os.alert({
|
||||||
title: this.$ts.uploadFromUrlRequested,
|
title: i18n.locale.uploadFromUrlRequested,
|
||||||
text: this.$ts.uploadFromUrlMayTakeTime
|
text: i18n.locale.uploadFromUrlMayTakeTime
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
createFolder() {
|
function createFolder() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: this.$ts.createFolder,
|
title: i18n.locale.createFolder,
|
||||||
placeholder: this.$ts.folderName
|
placeholder: i18n.locale.folderName
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/create', {
|
os.api('drive/folders/create', {
|
||||||
name: name,
|
name: name,
|
||||||
parentId: this.folder ? this.folder.id : undefined
|
parentId: folder.value ? folder.value.id : undefined
|
||||||
}).then(folder => {
|
}).then(createdFolder => {
|
||||||
this.addFolder(folder, true);
|
addFolder(createdFolder, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
renameFolder(folder) {
|
function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: this.$ts.renameFolder,
|
title: i18n.locale.renameFolder,
|
||||||
placeholder: this.$ts.inputNewFolderName,
|
placeholder: i18n.locale.inputNewFolderName,
|
||||||
default: folder.name
|
default: folderToRename.name
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: folder.id,
|
folderId: folderToRename.id,
|
||||||
name: name
|
name: name
|
||||||
}).then(folder => {
|
}).then(updatedFolder => {
|
||||||
// FIXME: 画面を更新するために自分自身に移動
|
// FIXME: 画面を更新するために自分自身に移動
|
||||||
this.move(folder);
|
move(updatedFolder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
deleteFolder(folder) {
|
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
||||||
os.api('drive/folders/delete', {
|
os.api('drive/folders/delete', {
|
||||||
folderId: folder.id
|
folderId: folderToDelete.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 削除時に親フォルダに移動
|
// 削除時に親フォルダに移動
|
||||||
this.move(folder.parentId);
|
move(folderToDelete.parentId);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
switch(err.id) {
|
switch(err.id) {
|
||||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: this.$ts.unableToDelete,
|
title: i18n.locale.unableToDelete,
|
||||||
text: this.$ts.hasChildFilesOrFolders
|
text: i18n.locale.hasChildFilesOrFolders
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.unableToDelete
|
text: i18n.locale.unableToDelete
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
onChangeFileInput() {
|
function onChangeFileInput() {
|
||||||
for (const file of Array.from((this.$refs.fileInput as any).files)) {
|
if (!fileInput.value?.files) return;
|
||||||
this.upload(file, this.folder);
|
for (const file of Array.from(fileInput.value.files)) {
|
||||||
|
upload(file, folder.value);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
upload(file, folder) {
|
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
||||||
if (folder && typeof folder == 'object') folder = folder.id;
|
os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
|
||||||
os.upload(file, folder).then(res => {
|
addFile(res, true);
|
||||||
this.addFile(res, true);
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
chooseFile(file) {
|
function chooseFile(file: Misskey.entities.DriveFile) {
|
||||||
const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
|
const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
|
||||||
if (this.multiple) {
|
if (props.multiple) {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
|
selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
|
||||||
} else {
|
} else {
|
||||||
this.selectedFiles.push(file);
|
selectedFiles.value.push(file);
|
||||||
}
|
}
|
||||||
this.$emit('change-selection', this.selectedFiles);
|
emit('change-selection', selectedFiles.value);
|
||||||
} else {
|
} else {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
this.$emit('selected', file);
|
emit('selected', file);
|
||||||
} else {
|
} else {
|
||||||
this.selectedFiles = [file];
|
selectedFiles.value = [file];
|
||||||
this.$emit('change-selection', [file]);
|
emit('change-selection', [file]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
chooseFolder(folder) {
|
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
||||||
const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
|
const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
|
||||||
if (this.multiple) {
|
if (props.multiple) {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
|
selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
|
||||||
} else {
|
} else {
|
||||||
this.selectedFolders.push(folder);
|
selectedFolders.value.push(folderToChoose);
|
||||||
}
|
}
|
||||||
this.$emit('change-selection', this.selectedFolders);
|
emit('change-selection', selectedFolders.value);
|
||||||
} else {
|
} else {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
this.$emit('selected', folder);
|
emit('selected', folderToChoose);
|
||||||
} else {
|
} else {
|
||||||
this.selectedFolders = [folder];
|
selectedFolders.value = [folderToChoose];
|
||||||
this.$emit('change-selection', [folder]);
|
emit('change-selection', [folderToChoose]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
move(target) {
|
function move(target?: Misskey.entities.DriveFolder) {
|
||||||
if (target == null) {
|
if (!target) {
|
||||||
this.goRoot();
|
goRoot();
|
||||||
return;
|
return;
|
||||||
} else if (typeof target == 'object') {
|
} else if (typeof target == 'object') {
|
||||||
target = target.id;
|
target = target.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetching = true;
|
fetching.value = true;
|
||||||
|
|
||||||
os.api('drive/folders/show', {
|
os.api('drive/folders/show', {
|
||||||
folderId: target
|
folderId: target
|
||||||
}).then(folder => {
|
}).then(folderToMove => {
|
||||||
this.folder = folder;
|
folder.value = folderToMove;
|
||||||
this.hierarchyFolders = [];
|
hierarchyFolders.value = [];
|
||||||
|
|
||||||
const dive = folder => {
|
const dive = folderToDive => {
|
||||||
this.hierarchyFolders.unshift(folder);
|
hierarchyFolders.value.unshift(folderToDive);
|
||||||
if (folder.parent) dive(folder.parent);
|
if (folderToDive.parent) dive(folderToDive.parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (folder.parent) dive(folder.parent);
|
if (folderToMove.parent) dive(folderToMove.parent);
|
||||||
|
|
||||||
this.$emit('open-folder', folder);
|
emit('open-folder', folderToMove);
|
||||||
this.fetch();
|
fetch();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
addFolder(folder, unshift = false) {
|
function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
|
||||||
const current = this.folder ? this.folder.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current != folder.parentId) return;
|
if (current != folderToAdd.parentId) return;
|
||||||
|
|
||||||
if (this.folders.some(f => f.id == folder.id)) {
|
if (folders.value.some(f => f.id == folderToAdd.id)) {
|
||||||
const exist = this.folders.map(f => f.id).indexOf(folder.id);
|
const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
|
||||||
this.folders[exist] = folder;
|
folders.value[exist] = folderToAdd;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unshift) {
|
if (unshift) {
|
||||||
this.folders.unshift(folder);
|
folders.value.unshift(folderToAdd);
|
||||||
} else {
|
} else {
|
||||||
this.folders.push(folder);
|
folders.value.push(folderToAdd);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
addFile(file, unshift = false) {
|
function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
|
||||||
const current = this.folder ? this.folder.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current != file.folderId) return;
|
if (current != fileToAdd.folderId) return;
|
||||||
|
|
||||||
if (this.files.some(f => f.id == file.id)) {
|
if (files.value.some(f => f.id == fileToAdd.id)) {
|
||||||
const exist = this.files.map(f => f.id).indexOf(file.id);
|
const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
|
||||||
this.files[exist] = file;
|
files.value[exist] = fileToAdd;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unshift) {
|
if (unshift) {
|
||||||
this.files.unshift(file);
|
files.value.unshift(fileToAdd);
|
||||||
} else {
|
} else {
|
||||||
this.files.push(file);
|
files.value.push(fileToAdd);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
removeFolder(folder) {
|
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
|
||||||
if (typeof folder == 'object') folder = folder.id;
|
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
|
||||||
this.folders = this.folders.filter(f => f.id != folder);
|
folders.value = folders.value.filter(f => f.id != folderIdToRemove);
|
||||||
},
|
}
|
||||||
|
|
||||||
removeFile(file) {
|
function removeFile(file: Misskey.entities.DriveFile | string) {
|
||||||
if (typeof file == 'object') file = file.id;
|
const fileId = typeof file === 'object' ? file.id : file;
|
||||||
this.files = this.files.filter(f => f.id != file);
|
files.value = files.value.filter(f => f.id != fileId);
|
||||||
},
|
}
|
||||||
|
|
||||||
appendFile(file) {
|
function appendFile(file: Misskey.entities.DriveFile) {
|
||||||
this.addFile(file);
|
addFile(file);
|
||||||
},
|
}
|
||||||
|
|
||||||
appendFolder(folder) {
|
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
|
||||||
this.addFolder(folder);
|
addFolder(folderToAppend);
|
||||||
},
|
}
|
||||||
|
/*
|
||||||
|
function prependFile(file: Misskey.entities.DriveFile) {
|
||||||
|
addFile(file, true);
|
||||||
|
}
|
||||||
|
|
||||||
prependFile(file) {
|
function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
|
||||||
this.addFile(file, true);
|
addFolder(folderToPrepend, true);
|
||||||
},
|
}
|
||||||
|
*/
|
||||||
prependFolder(folder) {
|
function goRoot() {
|
||||||
this.addFolder(folder, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
goRoot() {
|
|
||||||
// 既にrootにいるなら何もしない
|
// 既にrootにいるなら何もしない
|
||||||
if (this.folder == null) return;
|
if (folder.value == null) return;
|
||||||
|
|
||||||
this.folder = null;
|
folder.value = null;
|
||||||
this.hierarchyFolders = [];
|
hierarchyFolders.value = [];
|
||||||
this.$emit('move-root');
|
emit('move-root');
|
||||||
this.fetch();
|
fetch();
|
||||||
},
|
}
|
||||||
|
|
||||||
fetch() {
|
async function fetch() {
|
||||||
this.folders = [];
|
folders.value = [];
|
||||||
this.files = [];
|
files.value = [];
|
||||||
this.moreFolders = false;
|
moreFolders.value = false;
|
||||||
this.moreFiles = false;
|
moreFiles.value = false;
|
||||||
this.fetching = true;
|
fetching.value = true;
|
||||||
|
|
||||||
let fetchedFolders = null;
|
|
||||||
let fetchedFiles = null;
|
|
||||||
|
|
||||||
const foldersMax = 30;
|
const foldersMax = 30;
|
||||||
const filesMax = 30;
|
const filesMax = 30;
|
||||||
|
|
||||||
// フォルダ一覧取得
|
const foldersPromise = os.api('drive/folders', {
|
||||||
os.api('drive/folders', {
|
folderId: folder.value ? folder.value.id : null,
|
||||||
folderId: this.folder ? this.folder.id : null,
|
|
||||||
limit: foldersMax + 1
|
limit: foldersMax + 1
|
||||||
}).then(folders => {
|
}).then(fetchedFolders => {
|
||||||
if (folders.length == foldersMax + 1) {
|
if (fetchedFolders.length == foldersMax + 1) {
|
||||||
this.moreFolders = true;
|
moreFolders.value = true;
|
||||||
folders.pop();
|
fetchedFolders.pop();
|
||||||
}
|
}
|
||||||
fetchedFolders = folders;
|
return fetchedFolders;
|
||||||
complete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ファイル一覧取得
|
const filesPromise = os.api('drive/files', {
|
||||||
os.api('drive/files', {
|
folderId: folder.value ? folder.value.id : null,
|
||||||
folderId: this.folder ? this.folder.id : null,
|
type: props.type,
|
||||||
type: this.type,
|
|
||||||
limit: filesMax + 1
|
limit: filesMax + 1
|
||||||
}).then(files => {
|
}).then(fetchedFiles => {
|
||||||
if (files.length == filesMax + 1) {
|
if (fetchedFiles.length == filesMax + 1) {
|
||||||
this.moreFiles = true;
|
moreFiles.value = true;
|
||||||
files.pop();
|
fetchedFiles.pop();
|
||||||
}
|
}
|
||||||
fetchedFiles = files;
|
return fetchedFiles;
|
||||||
complete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let flag = false;
|
const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
|
||||||
const complete = () => {
|
|
||||||
if (flag) {
|
|
||||||
for (const x of fetchedFolders) this.appendFolder(x);
|
|
||||||
for (const x of fetchedFiles) this.appendFile(x);
|
|
||||||
this.fetching = false;
|
|
||||||
} else {
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMoreFiles() {
|
for (const x of fetchedFolders) appendFolder(x);
|
||||||
this.fetching = true;
|
for (const x of fetchedFiles) appendFile(x);
|
||||||
|
|
||||||
|
fetching.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMoreFiles() {
|
||||||
|
fetching.value = true;
|
||||||
|
|
||||||
const max = 30;
|
const max = 30;
|
||||||
|
|
||||||
// ファイル一覧取得
|
// ファイル一覧取得
|
||||||
os.api('drive/files', {
|
os.api('drive/files', {
|
||||||
folderId: this.folder ? this.folder.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: this.type,
|
type: props.type,
|
||||||
untilId: this.files[this.files.length - 1].id,
|
untilId: files.value[files.value.length - 1].id,
|
||||||
limit: max + 1
|
limit: max + 1
|
||||||
}).then(files => {
|
}).then(files => {
|
||||||
if (files.length == max + 1) {
|
if (files.length == max + 1) {
|
||||||
this.moreFiles = true;
|
moreFiles.value = true;
|
||||||
files.pop();
|
files.pop();
|
||||||
} else {
|
} else {
|
||||||
this.moreFiles = false;
|
moreFiles.value = false;
|
||||||
}
|
}
|
||||||
for (const x of files) this.appendFile(x);
|
for (const x of files) appendFile(x);
|
||||||
this.fetching = false;
|
fetching.value = false;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getMenu() {
|
function getMenu() {
|
||||||
return [{
|
return [{
|
||||||
text: this.$ts.addFile,
|
text: i18n.locale.addFile,
|
||||||
type: 'label'
|
type: 'label'
|
||||||
}, {
|
}, {
|
||||||
text: this.$ts.upload,
|
text: i18n.locale.upload,
|
||||||
icon: 'fas fa-upload',
|
icon: 'fas fa-upload',
|
||||||
action: () => { this.selectLocalFile(); }
|
action: () => { selectLocalFile(); }
|
||||||
}, {
|
}, {
|
||||||
text: this.$ts.fromUrl,
|
text: i18n.locale.fromUrl,
|
||||||
icon: 'fas fa-link',
|
icon: 'fas fa-link',
|
||||||
action: () => { this.urlUpload(); }
|
action: () => { urlUpload(); }
|
||||||
}, null, {
|
}, null, {
|
||||||
text: this.folder ? this.folder.name : this.$ts.drive,
|
text: folder.value ? folder.value.name : i18n.locale.drive,
|
||||||
type: 'label'
|
type: 'label'
|
||||||
}, this.folder ? {
|
}, folder.value ? {
|
||||||
text: this.$ts.renameFolder,
|
text: i18n.locale.renameFolder,
|
||||||
icon: 'fas fa-i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
action: () => { this.renameFolder(this.folder); }
|
action: () => { renameFolder(folder.value); }
|
||||||
} : undefined, this.folder ? {
|
} : undefined, folder.value ? {
|
||||||
text: this.$ts.deleteFolder,
|
text: i18n.locale.deleteFolder,
|
||||||
icon: 'fas fa-trash-alt',
|
icon: 'fas fa-trash-alt',
|
||||||
action: () => { this.deleteFolder(this.folder); }
|
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
|
||||||
} : undefined, {
|
} : undefined, {
|
||||||
text: this.$ts.createFolder,
|
text: i18n.locale.createFolder,
|
||||||
icon: 'fas fa-folder-plus',
|
icon: 'fas fa-folder-plus',
|
||||||
action: () => { this.createFolder(); }
|
action: () => { createFolder(); }
|
||||||
}];
|
}];
|
||||||
},
|
}
|
||||||
|
|
||||||
showMenu(ev) {
|
function showMenu(ev: MouseEvent) {
|
||||||
os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
|
os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
|
||||||
},
|
}
|
||||||
|
|
||||||
onContextmenu(ev) {
|
function onContextmenu(ev: MouseEvent) {
|
||||||
os.contextMenu(this.getMenu(), ev);
|
os.contextMenu(getMenu(), ev);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
ilFilesObserver.observe(loadMoreFiles.value?.$el)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection.on('fileCreated', onStreamDriveFileCreated);
|
||||||
|
connection.on('fileUpdated', onStreamDriveFileUpdated);
|
||||||
|
connection.on('fileDeleted', onStreamDriveFileDeleted);
|
||||||
|
connection.on('folderCreated', onStreamDriveFolderCreated);
|
||||||
|
connection.on('folderUpdated', onStreamDriveFolderUpdated);
|
||||||
|
connection.on('folderDeleted', onStreamDriveFolderDeleted);
|
||||||
|
|
||||||
|
if (props.initialFolder) {
|
||||||
|
move(props.initialFolder);
|
||||||
|
} else {
|
||||||
|
fetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (defaultStore.state.enableInfiniteScroll) {
|
||||||
|
nextTick(() => {
|
||||||
|
ilFilesObserver.observe(loadMoreFiles.value?.$el)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
connection.dispose();
|
||||||
|
ilFilesObserver.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,58 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
|
<MkModal
|
||||||
<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
|
ref="modal"
|
||||||
|
v-slot="{ type, maxHeight }"
|
||||||
|
:z-priority="'middle'"
|
||||||
|
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||||
|
:transparent-bg="true"
|
||||||
|
:manual-showing="manualShowing"
|
||||||
|
:src="src"
|
||||||
|
@click="modal?.close()"
|
||||||
|
@opening="opening"
|
||||||
|
@close="emit('close')"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<MkEmojiPicker
|
||||||
|
ref="picker"
|
||||||
|
class="ryghynhb _popup _shadow"
|
||||||
|
:class="{ drawer: type === 'drawer' }"
|
||||||
|
:show-pinned="showPinned"
|
||||||
|
:as-reaction-picker="asReactionPicker"
|
||||||
|
:as-drawer="type === 'drawer'"
|
||||||
|
:max-height="maxHeight"
|
||||||
|
@chosen="chosen"
|
||||||
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MkModal from '@/components/ui/modal.vue';
|
import MkModal from '@/components/ui/modal.vue';
|
||||||
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
export default defineComponent({
|
withDefaults(defineProps<{
|
||||||
components: {
|
manualShowing?: boolean;
|
||||||
MkModal,
|
src?: HTMLElement;
|
||||||
MkEmojiPicker,
|
showPinned?: boolean;
|
||||||
},
|
asReactionPicker?: boolean;
|
||||||
|
}>(), {
|
||||||
props: {
|
manualShowing: false,
|
||||||
manualShowing: {
|
showPinned: true,
|
||||||
type: Boolean,
|
asReactionPicker: false,
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
src: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
showPinned: {
|
|
||||||
required: false,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
asReactionPicker: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['done', 'close', 'closed'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
chosen(emoji: any) {
|
|
||||||
this.$emit('done', emoji);
|
|
||||||
this.$refs.modal.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
opening() {
|
|
||||||
this.$refs.picker.reset();
|
|
||||||
this.$refs.picker.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'done', v: any): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
(e: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof MkModal>>();
|
||||||
|
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
|
function chosen(emoji: any) {
|
||||||
|
emit('done', emoji);
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function opening() {
|
||||||
|
picker.value?.reset();
|
||||||
|
picker.value?.focus();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -5,50 +5,33 @@
|
|||||||
:can-resize="false"
|
:can-resize="false"
|
||||||
:mini="true"
|
:mini="true"
|
||||||
:front="true"
|
:front="true"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { } from 'vue';
|
||||||
import MkWindow from '@/components/ui/window.vue';
|
import MkWindow from '@/components/ui/window.vue';
|
||||||
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
withDefaults(defineProps<{
|
||||||
components: {
|
src?: HTMLElement;
|
||||||
MkWindow,
|
showPinned?: boolean;
|
||||||
MkEmojiPicker,
|
asReactionPicker?: boolean;
|
||||||
},
|
}>(), {
|
||||||
|
showPinned: true,
|
||||||
props: {
|
|
||||||
src: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
showPinned: {
|
|
||||||
required: false,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
asReactionPicker: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['chosen', 'closed'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
chosen(emoji: any) {
|
|
||||||
this.$emit('chosen', emoji);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'chosen', v: any): void;
|
||||||
|
(e: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function chosen(emoji: any) {
|
||||||
|
emit('chosen', emoji);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<button v-for="emoji in emojis"
|
<button v-for="emoji in emojis"
|
||||||
:key="emoji"
|
:key="emoji"
|
||||||
class="_button"
|
class="_button"
|
||||||
@click="chosen(emoji, $event)"
|
@click="emit('chosen', emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji :emoji="emoji" :normal="true"/>
|
<MkEmoji :emoji="emoji" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
@ -15,35 +15,19 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
emojis: string[];
|
||||||
emojis: {
|
initialShown?: boolean;
|
||||||
required: true,
|
}>();
|
||||||
},
|
|
||||||
initialShown: {
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['chosen'],
|
const emit = defineEmits<{
|
||||||
|
(e: 'chosen', v: string, ev: MouseEvent): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
data() {
|
const shown = ref(!!props.initialShown);
|
||||||
return {
|
|
||||||
getStaticImageUrl,
|
|
||||||
shown: this.initialShown,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
chosen(emoji: any, ev) {
|
|
||||||
this.$parent.chosen(emoji, ev);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
|
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, 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="$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.locale.search" @paste.stop="paste" @keyup.enter="done()">
|
||||||
<div ref="emojis" class="emojis">
|
<div ref="emojis" class="emojis">
|
||||||
<section class="result">
|
<section class="result">
|
||||||
<div v-if="searchResultCustom.length > 0">
|
<div v-if="searchResultCustom.length > 0">
|
||||||
<button v-for="emoji in searchResultCustom"
|
<button v-for="emoji in searchResultCustom"
|
||||||
:key="emoji"
|
:key="emoji.id"
|
||||||
class="_button"
|
class="_button"
|
||||||
:title="emoji.name"
|
:title="emoji.name"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
|
||||||
<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
<img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="searchResultUnicode.length > 0">
|
<div v-if="searchResultUnicode.length > 0">
|
||||||
@ -43,9 +43,9 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
|
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
|
||||||
<div>
|
<div>
|
||||||
<button v-for="emoji in $store.state.recentlyUsedEmojis"
|
<button v-for="emoji in recentlyUsedEmojis"
|
||||||
:key="emoji"
|
:key="emoji"
|
||||||
class="_button"
|
class="_button"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
@ -56,12 +56,12 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<header class="_acrylic">{{ $ts.customEmojis }}</header>
|
<header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
|
||||||
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
|
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<header class="_acrylic">{{ $ts.emoji }}</header>
|
<header class="_acrylic">{{ i18n.locale.emoji }}</header>
|
||||||
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
|
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
@ -73,82 +73,75 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { emojilist } from '@/scripts/emojilist';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||||
import Ripple from '@/components/ripple.vue';
|
import Ripple from '@/components/ripple.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
import { isTouchUsing } from '@/scripts/touch';
|
||||||
import { isMobile } from '@/scripts/is-mobile';
|
import { isMobile } from '@/scripts/is-mobile';
|
||||||
import { emojiCategories } from '@/instance';
|
import { emojiCategories, instance } from '@/instance';
|
||||||
import XSection from './emoji-picker.section.vue';
|
import XSection from './emoji-picker.section.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
components: {
|
showPinned?: boolean;
|
||||||
XSection
|
asReactionPicker?: boolean;
|
||||||
},
|
maxHeight?: number;
|
||||||
|
asDrawer?: boolean;
|
||||||
|
}>(), {
|
||||||
|
showPinned: true,
|
||||||
|
});
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
showPinned: {
|
(e: 'chosen', v: string): void;
|
||||||
required: false,
|
}>();
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
asReactionPicker: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
maxHeight: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
asDrawer: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['chosen'],
|
const search = ref<HTMLInputElement>();
|
||||||
|
const emojis = ref<HTMLDivElement>();
|
||||||
|
|
||||||
data() {
|
const {
|
||||||
return {
|
reactions: pinned,
|
||||||
emojilist: markRaw(emojilist),
|
reactionPickerWidth,
|
||||||
getStaticImageUrl,
|
reactionPickerHeight,
|
||||||
pinned: this.$store.reactiveState.reactions,
|
disableShowingAnimatedImages,
|
||||||
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
|
recentlyUsedEmojis,
|
||||||
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
|
} = defaultStore.reactiveState;
|
||||||
big: this.asReactionPicker ? isTouchUsing : false,
|
|
||||||
customEmojiCategories: emojiCategories,
|
|
||||||
customEmojis: this.$instance.emojis,
|
|
||||||
q: null,
|
|
||||||
searchResultCustom: [],
|
|
||||||
searchResultUnicode: [],
|
|
||||||
tab: 'index',
|
|
||||||
categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
||||||
q() {
|
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
||||||
this.$refs.emojis.scrollTop = 0;
|
const big = props.asReactionPicker ? isTouchUsing : false;
|
||||||
|
const customEmojiCategories = emojiCategories;
|
||||||
|
const customEmojis = instance.emojis;
|
||||||
|
const q = ref<string | null>(null);
|
||||||
|
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
||||||
|
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||||
|
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||||
|
|
||||||
if (this.q == null || this.q === '') {
|
watch(q, () => {
|
||||||
this.searchResultCustom = [];
|
if (emojis.value) emojis.value.scrollTop = 0;
|
||||||
this.searchResultUnicode = [];
|
|
||||||
|
if (q.value == null || q.value === '') {
|
||||||
|
searchResultCustom.value = [];
|
||||||
|
searchResultUnicode.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const q = this.q.replace(/:/g, '');
|
const newQ = q.value.replace(/:/g, '');
|
||||||
|
|
||||||
const searchCustom = () => {
|
const searchCustom = () => {
|
||||||
const max = 8;
|
const max = 8;
|
||||||
const emojis = this.customEmojis;
|
const emojis = customEmojis;
|
||||||
const matches = new Set();
|
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(e => e.name === q);
|
const exactMatch = emojis.find(e => e.name === newQ);
|
||||||
if (exactMatch) matches.add(exactMatch);
|
if (exactMatch) matches.add(exactMatch);
|
||||||
|
|
||||||
if (q.includes(' ')) { // AND検索
|
if (newQ.includes(' ')) { // AND検索
|
||||||
const keywords = q.split(' ');
|
const keywords = newQ.split(' ');
|
||||||
|
|
||||||
// 名前にキーワードが含まれている
|
// 名前にキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
@ -168,7 +161,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.name.startsWith(q)) {
|
if (emoji.name.startsWith(newQ)) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -176,7 +169,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.aliases.some(alias => alias.startsWith(q))) {
|
if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -184,7 +177,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.name.includes(q)) {
|
if (emoji.name.includes(newQ)) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -192,7 +185,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.aliases.some(alias => alias.includes(q))) {
|
if (emoji.aliases.some(alias => alias.includes(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -204,14 +197,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
const searchUnicode = () => {
|
const searchUnicode = () => {
|
||||||
const max = 8;
|
const max = 8;
|
||||||
const emojis = this.emojilist;
|
const emojis = emojilist;
|
||||||
const matches = new Set();
|
const matches = new Set<UnicodeEmojiDef>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(e => e.name === q);
|
const exactMatch = emojis.find(e => e.name === newQ);
|
||||||
if (exactMatch) matches.add(exactMatch);
|
if (exactMatch) matches.add(exactMatch);
|
||||||
|
|
||||||
if (q.includes(' ')) { // AND検索
|
if (newQ.includes(' ')) { // AND検索
|
||||||
const keywords = q.split(' ');
|
const keywords = newQ.split(' ');
|
||||||
|
|
||||||
// 名前にキーワードが含まれている
|
// 名前にキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
@ -231,7 +224,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.name.startsWith(q)) {
|
if (emoji.name.startsWith(newQ)) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -239,7 +232,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
|
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -247,7 +240,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.name.includes(q)) {
|
if (emoji.name.includes(newQ)) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -255,7 +248,7 @@ export default defineComponent({
|
|||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.keywords.some(keyword => keyword.includes(q))) {
|
if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
@ -265,85 +258,87 @@ export default defineComponent({
|
|||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.searchResultCustom = Array.from(searchCustom());
|
searchResultCustom.value = Array.from(searchCustom());
|
||||||
this.searchResultUnicode = Array.from(searchUnicode());
|
searchResultUnicode.value = Array.from(searchUnicode());
|
||||||
}
|
});
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
function focus() {
|
||||||
this.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
if (!isMobile && !isTouchUsing) {
|
if (!isMobile && !isTouchUsing) {
|
||||||
this.$refs.search.focus({
|
search.value?.focus({
|
||||||
preventScroll: true
|
preventScroll: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
reset() {
|
function reset() {
|
||||||
this.$refs.emojis.scrollTop = 0;
|
if (emojis.value) emojis.value.scrollTop = 0;
|
||||||
this.q = '';
|
q.value = '';
|
||||||
},
|
}
|
||||||
|
|
||||||
getKey(emoji: any) {
|
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
|
||||||
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
|
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
|
||||||
},
|
}
|
||||||
|
|
||||||
chosen(emoji: any, ev) {
|
function chosen(emoji: any, ev?: MouseEvent) {
|
||||||
if (ev) {
|
const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
|
||||||
const el = ev.currentTarget || ev.target;
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
os.popup(Ripple, { x, y }, {}, 'end');
|
os.popup(Ripple, { x, y }, {}, 'end');
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = this.getKey(emoji);
|
const key = getKey(emoji);
|
||||||
this.$emit('chosen', key);
|
emit('chosen', key);
|
||||||
|
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!this.pinned.includes(key)) {
|
if (!pinned.value.includes(key)) {
|
||||||
let recents = this.$store.state.recentlyUsedEmojis;
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||||
recents = recents.filter((e: any) => e !== key);
|
recents = recents.filter((e: any) => e !== key);
|
||||||
recents.unshift(key);
|
recents.unshift(key);
|
||||||
this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
paste(event) {
|
function paste(event: ClipboardEvent) {
|
||||||
const paste = (event.clipboardData || window.clipboardData).getData('text');
|
const paste = (event.clipboardData || window.clipboardData).getData('text');
|
||||||
if (this.done(paste)) {
|
if (done(paste)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
done(query) {
|
function done(query?: any): boolean | void {
|
||||||
if (query == null) query = this.q;
|
if (query == null) query = q.value;
|
||||||
if (query == null) return;
|
if (query == null || typeof query !== 'string') return;
|
||||||
const q = query.replace(/:/g, '');
|
|
||||||
const exactMatchCustom = this.customEmojis.find(e => e.name === q);
|
const q2 = query.replace(/:/g, '');
|
||||||
|
const exactMatchCustom = customEmojis.find(e => e.name === q2);
|
||||||
if (exactMatchCustom) {
|
if (exactMatchCustom) {
|
||||||
this.chosen(exactMatchCustom);
|
chosen(exactMatchCustom);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
|
const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
|
||||||
if (exactMatchUnicode) {
|
if (exactMatchUnicode) {
|
||||||
this.chosen(exactMatchUnicode);
|
chosen(exactMatchUnicode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.searchResultCustom.length > 0) {
|
if (searchResultCustom.value.length > 0) {
|
||||||
this.chosen(this.searchResultCustom[0]);
|
chosen(searchResultCustom.value[0]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.searchResultUnicode.length > 0) {
|
if (searchResultUnicode.value.length > 0) {
|
||||||
this.chosen(this.searchResultUnicode[0]);
|
chosen(searchResultUnicode.value[0]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
onMounted(() => {
|
||||||
|
focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
reset,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,25 +2,15 @@
|
|||||||
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
|
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
|
||||||
components: {
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
os.api('meta', { detail: true }).then(gotMeta => {
|
||||||
return {
|
meta.value = gotMeta;
|
||||||
meta: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
os.api('meta', { detail: true }).then(meta => {
|
|
||||||
this.meta = meta;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -6,129 +6,110 @@
|
|||||||
>
|
>
|
||||||
<template v-if="!wait">
|
<template v-if="!wait">
|
||||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||||
<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
|
<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
|
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
|
||||||
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
|
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isFollowing">
|
<template v-else-if="isFollowing">
|
||||||
<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
|
<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && user.isLocked">
|
<template v-else-if="!isFollowing && user.isLocked">
|
||||||
<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
|
<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && !user.isLocked">
|
<template v-else-if="!isFollowing && !user.isLocked">
|
||||||
<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
|
<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
|
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
props: {
|
user: Misskey.entities.UserDetailed,
|
||||||
user: {
|
full?: boolean,
|
||||||
type: Object,
|
large?: boolean,
|
||||||
required: true
|
}>(), {
|
||||||
},
|
full: false,
|
||||||
full: {
|
large: false,
|
||||||
type: Boolean,
|
});
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const isFollowing = ref(props.user.isFollowing);
|
||||||
return {
|
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
|
||||||
isFollowing: this.user.isFollowing,
|
const wait = ref(false);
|
||||||
hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
|
const connection = stream.useChannel('main');
|
||||||
wait: false,
|
|
||||||
connection: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
if (props.user.isFollowing == null) {
|
||||||
// 渡されたユーザー情報が不完全な場合
|
|
||||||
if (this.user.isFollowing == null) {
|
|
||||||
os.api('users/show', {
|
os.api('users/show', {
|
||||||
userId: this.user.id
|
userId: props.user.id
|
||||||
}).then(u => {
|
}).then(u => {
|
||||||
this.isFollowing = u.isFollowing;
|
isFollowing.value = u.isFollowing;
|
||||||
this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
|
hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||||
|
if (user.id == props.user.id) {
|
||||||
|
isFollowing.value = user.isFollowing;
|
||||||
|
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
mounted() {
|
async function onClick() {
|
||||||
this.connection = markRaw(stream.useChannel('main'));
|
wait.value = true;
|
||||||
|
|
||||||
this.connection.on('follow', this.onFollowChange);
|
|
||||||
this.connection.on('unfollow', this.onFollowChange);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
this.connection.dispose();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onFollowChange(user) {
|
|
||||||
if (user.id == this.user.id) {
|
|
||||||
this.isFollowing = user.isFollowing;
|
|
||||||
this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async onClick() {
|
|
||||||
this.wait = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.isFollowing) {
|
if (isFollowing.value) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
|
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await os.api('following/delete', {
|
await os.api('following/delete', {
|
||||||
userId: this.user.id
|
userId: props.user.id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (this.hasPendingFollowRequestFromYou) {
|
if (hasPendingFollowRequestFromYou.value) {
|
||||||
await os.api('following/requests/cancel', {
|
await os.api('following/requests/cancel', {
|
||||||
userId: this.user.id
|
userId: props.user.id
|
||||||
});
|
});
|
||||||
} else if (this.user.isLocked) {
|
} else if (props.user.isLocked) {
|
||||||
await os.api('following/create', {
|
await os.api('following/create', {
|
||||||
userId: this.user.id
|
userId: props.user.id
|
||||||
});
|
});
|
||||||
this.hasPendingFollowRequestFromYou = true;
|
hasPendingFollowRequestFromYou.value = true;
|
||||||
} else {
|
} else {
|
||||||
await os.api('following/create', {
|
await os.api('following/create', {
|
||||||
userId: this.user.id
|
userId: props.user.id
|
||||||
});
|
});
|
||||||
this.hasPendingFollowRequestFromYou = true;
|
hasPendingFollowRequestFromYou.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
this.wait = false;
|
wait.value = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connection.on('follow', onFollowChange);
|
||||||
|
connection.on('unfollow', onFollowChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
connection.dispose();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,72 +2,64 @@
|
|||||||
<XModalWindow ref="dialog"
|
<XModalWindow ref="dialog"
|
||||||
:width="370"
|
:width="370"
|
||||||
:height="400"
|
:height="400"
|
||||||
@close="$refs.dialog.close()"
|
@close="dialog.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ $ts.forgotPassword }}</template>
|
<template #header>{{ i18n.locale.forgotPassword }}</template>
|
||||||
|
|
||||||
<form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
||||||
<div class="main _formRoot">
|
<div class="main _formRoot">
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
|
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
|
||||||
<template #label>{{ $ts.username }}</template>
|
<template #label>{{ i18n.locale.username }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
|
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
|
||||||
<template #label>{{ $ts.emailAddress }}</template>
|
<template #label>{{ i18n.locale.emailAddress }}</template>
|
||||||
<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
|
<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
|
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
|
<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div v-else>
|
<div v-else class="bafecedb">
|
||||||
{{ $ts._forgotPassword.contactAdmin }}
|
{{ i18n.locale._forgotPassword.contactAdmin }}
|
||||||
</div>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { } from 'vue';
|
||||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const emit = defineEmits<{
|
||||||
components: {
|
(e: 'done'): void;
|
||||||
XModalWindow,
|
(e: 'closed'): void;
|
||||||
MkButton,
|
}>();
|
||||||
MkInput,
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['done', 'closed'],
|
let dialog: InstanceType<typeof XModalWindow> = $ref();
|
||||||
|
|
||||||
data() {
|
let username = $ref('');
|
||||||
return {
|
let email = $ref('');
|
||||||
username: '',
|
let processing = $ref(false);
|
||||||
email: '',
|
|
||||||
processing: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
async function onSubmit() {
|
||||||
async onSubmit() {
|
processing = true;
|
||||||
this.processing = true;
|
|
||||||
await os.apiWithDialog('request-reset-password', {
|
await os.apiWithDialog('request-reset-password', {
|
||||||
username: this.username,
|
username,
|
||||||
email: this.email,
|
email,
|
||||||
});
|
});
|
||||||
|
emit('done');
|
||||||
this.$emit('done');
|
dialog.close();
|
||||||
this.$refs.dialog.close();
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -81,4 +73,8 @@ export default defineComponent({
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bafecedb {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -541,7 +541,7 @@ export const uploads = ref<{
|
|||||||
img: string;
|
img: string;
|
||||||
}[]>([]);
|
}[]>([]);
|
||||||
|
|
||||||
export function upload(file: File, folder?: any, name?: string) {
|
export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
|
||||||
if (folder && typeof folder == 'object') folder = folder.id;
|
if (folder && typeof folder == 'object') folder = folder.id;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
|
export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
|
||||||
export const emojilist = require('../emojilist.json') as {
|
|
||||||
|
export type UnicodeEmojiDef = {
|
||||||
name: string;
|
name: string;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
char: string;
|
char: string;
|
||||||
category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
|
category: typeof unicodeEmojiCategories[number];
|
||||||
}[];
|
}
|
||||||
|
|
||||||
|
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
|
||||||
|
export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[];
|
||||||
|
Loading…
Reference in New Issue
Block a user