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:
		@@ -1,5 +1,5 @@
 | 
			
		||||
<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 v-if="icon" class="icon">
 | 
			
		||||
			<i :class="icon"></i>
 | 
			
		||||
@@ -28,8 +28,8 @@
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<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="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</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">{{ i18n.locale.cancel }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<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>
 | 
			
		||||
@@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkSelect from '@/components/form/select.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
type Input = {
 | 
			
		||||
	type: HTMLInputElement['type'];
 | 
			
		||||
 
 | 
			
		||||
@@ -14,71 +14,42 @@
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
 | 
			
		||||
import { ColdDeviceStorage } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		ImgWithBlurhash
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		file: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		fit: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: 'cover'
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isContextmenuShowing: false,
 | 
			
		||||
			isDragging: false,
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	file: Misskey.entities.DriveFile;
 | 
			
		||||
	fit: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
 | 
			
		||||
			if (this.file.type.startsWith('image/')) return 'image';
 | 
			
		||||
			if (this.file.type.startsWith('video/')) return 'video';
 | 
			
		||||
			if (this.file.type === 'audio/midi') return 'midi';
 | 
			
		||||
			if (this.file.type.startsWith('audio/')) return 'audio';
 | 
			
		||||
			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 ([
 | 
			
		||||
					"application/zip",
 | 
			
		||||
					"application/x-cpio",
 | 
			
		||||
					"application/x-bzip",
 | 
			
		||||
					"application/x-bzip2",
 | 
			
		||||
					"application/java-archive",
 | 
			
		||||
					"application/x-rar-compressed",
 | 
			
		||||
					"application/x-tar",
 | 
			
		||||
					"application/gzip",
 | 
			
		||||
					"application/x-7z-compressed"
 | 
			
		||||
				].some(e => e === this.file.type)) return 'archive';
 | 
			
		||||
			return 'unknown';
 | 
			
		||||
		},
 | 
			
		||||
		isThumbnailAvailable(): boolean {
 | 
			
		||||
			return this.file.thumbnailUrl
 | 
			
		||||
				? (this.is === 'image' || this.is === 'video')
 | 
			
		||||
				: 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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
const is = computed(() => {
 | 
			
		||||
	if (props.file.type.startsWith('image/')) return 'image';
 | 
			
		||||
	if (props.file.type.startsWith('video/')) return 'video';
 | 
			
		||||
	if (props.file.type === 'audio/midi') return 'midi';
 | 
			
		||||
	if (props.file.type.startsWith('audio/')) return 'audio';
 | 
			
		||||
	if (props.file.type.endsWith('/csv')) return 'csv';
 | 
			
		||||
	if (props.file.type.endsWith('/pdf')) return 'pdf';
 | 
			
		||||
	if (props.file.type.startsWith('text/')) return 'textfile';
 | 
			
		||||
	if ([
 | 
			
		||||
			"application/zip",
 | 
			
		||||
			"application/x-cpio",
 | 
			
		||||
			"application/x-bzip",
 | 
			
		||||
			"application/x-bzip2",
 | 
			
		||||
			"application/java-archive",
 | 
			
		||||
			"application/x-rar-compressed",
 | 
			
		||||
			"application/x-tar",
 | 
			
		||||
			"application/gzip",
 | 
			
		||||
			"application/x-7z-compressed"
 | 
			
		||||
		].some(e => e === props.file.type)) return 'archive';
 | 
			
		||||
	return 'unknown';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isThumbnailAvailable = computed(() => {
 | 
			
		||||
	return props.file.thumbnailUrl
 | 
			
		||||
		? (is.value === 'image' as const || is.value === 'video')
 | 
			
		||||
		: false;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,64 +7,51 @@
 | 
			
		||||
	@click="cancel()"
 | 
			
		||||
	@close="cancel()"
 | 
			
		||||
	@ok="ok()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<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>
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XDrive from './drive.vue';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XDrive,
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	type?: 'file' | 'folder';
 | 
			
		||||
	multiple: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	type: 'file',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,42 +3,27 @@
 | 
			
		||||
	:initial-width="800"
 | 
			
		||||
	:initial-height="500"
 | 
			
		||||
	:can-resize="true"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		{{ $ts.drive }}
 | 
			
		||||
		{{ i18n.locale.drive }}
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :initial-folder="initialFolder"/>
 | 
			
		||||
</XWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import {  } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XDrive from './drive.vue';
 | 
			
		||||
import XWindow from '@/components/ui/window.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XDrive,
 | 
			
		||||
		XWindow,
 | 
			
		||||
	},
 | 
			
		||||
defineProps<{
 | 
			
		||||
	initialFolder?: Misskey.entities.DriveFolder;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		initialFolder: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,17 @@
 | 
			
		||||
	@dragstart="onDragstart"
 | 
			
		||||
	@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"/>
 | 
			
		||||
		<p>{{ $ts.avatar }}</p>
 | 
			
		||||
		<p>{{ i18n.locale.avatar }}</p>
 | 
			
		||||
	</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"/>
 | 
			
		||||
		<p>{{ $ts.banner }}</p>
 | 
			
		||||
		<p>{{ i18n.locale.banner }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="file.isSensitive" class="label red">
 | 
			
		||||
		<img src="/client-assets/label-red.svg"/>
 | 
			
		||||
		<p>{{ $ts.nsfw }}</p>
 | 
			
		||||
		<p>{{ i18n.locale.nsfw }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 | 
			
		||||
@@ -30,179 +30,155 @@
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
 | 
			
		||||
import bytes from '@/filters/bytes';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkDriveFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		file: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		isSelected: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
		selectMode: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['chosen'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isDragging: false
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		// TODO: parentへの参照を無くす
 | 
			
		||||
		browser(): any {
 | 
			
		||||
			return this.$parent;
 | 
			
		||||
		},
 | 
			
		||||
		title(): string {
 | 
			
		||||
			return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		getMenu() {
 | 
			
		||||
			return [{
 | 
			
		||||
				text: this.$ts.rename,
 | 
			
		||||
				icon: 'fas fa-i-cursor',
 | 
			
		||||
				action: this.rename
 | 
			
		||||
			}, {
 | 
			
		||||
				text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
 | 
			
		||||
				icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
 | 
			
		||||
				action: this.toggleSensitive
 | 
			
		||||
			}, {
 | 
			
		||||
				text: this.$ts.describeFile,
 | 
			
		||||
				icon: 'fas fa-i-cursor',
 | 
			
		||||
				action: this.describe
 | 
			
		||||
			}, null, {
 | 
			
		||||
				text: this.$ts.copyUrl,
 | 
			
		||||
				icon: 'fas fa-link',
 | 
			
		||||
				action: this.copyUrl
 | 
			
		||||
			}, {
 | 
			
		||||
				type: 'a',
 | 
			
		||||
				href: this.file.url,
 | 
			
		||||
				target: '_blank',
 | 
			
		||||
				text: this.$ts.download,
 | 
			
		||||
				icon: 'fas fa-download',
 | 
			
		||||
				download: this.file.name
 | 
			
		||||
			}, null, {
 | 
			
		||||
				text: this.$ts.delete,
 | 
			
		||||
				icon: 'fas fa-trash-alt',
 | 
			
		||||
				danger: true,
 | 
			
		||||
				action: this.deleteFile
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onClick(ev) {
 | 
			
		||||
			if (this.selectMode) {
 | 
			
		||||
				this.$emit('chosen', this.file);
 | 
			
		||||
			} else {
 | 
			
		||||
				os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onContextmenu(e) {
 | 
			
		||||
			os.contextMenu(this.getMenu(), e);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragstart(e) {
 | 
			
		||||
			e.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
 | 
			
		||||
			this.isDragging = true;
 | 
			
		||||
 | 
			
		||||
			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
 | 
			
		||||
			// (=あなたの子供が、ドラッグを開始しましたよ)
 | 
			
		||||
			this.browser.isDragSource = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragend(e) {
 | 
			
		||||
			this.isDragging = false;
 | 
			
		||||
			this.browser.isDragSource = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		rename() {
 | 
			
		||||
			os.inputText({
 | 
			
		||||
				title: this.$ts.renameFile,
 | 
			
		||||
				placeholder: this.$ts.inputNewFileName,
 | 
			
		||||
				default: this.file.name,
 | 
			
		||||
				allowEmpty: false
 | 
			
		||||
			}).then(({ canceled, result: name }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				os.api('drive/files/update', {
 | 
			
		||||
					fileId: this.file.id,
 | 
			
		||||
					name: name
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		describe() {
 | 
			
		||||
			os.popup(import('@/components/media-caption.vue'), {
 | 
			
		||||
				title: this.$ts.describeFile,
 | 
			
		||||
				input: {
 | 
			
		||||
					placeholder: this.$ts.inputNewDescription,
 | 
			
		||||
					default: this.file.comment !== null ? this.file.comment : '',
 | 
			
		||||
				},
 | 
			
		||||
				image: this.file
 | 
			
		||||
			}, {
 | 
			
		||||
				done: result => {
 | 
			
		||||
					if (!result || result.canceled) return;
 | 
			
		||||
					let comment = result.result;
 | 
			
		||||
					os.api('drive/files/update', {
 | 
			
		||||
						fileId: this.file.id,
 | 
			
		||||
						comment: comment.length == 0 ? null : comment
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleSensitive() {
 | 
			
		||||
			os.api('drive/files/update', {
 | 
			
		||||
				fileId: this.file.id,
 | 
			
		||||
				isSensitive: !this.file.isSensitive
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		copyUrl() {
 | 
			
		||||
			copyToClipboard(this.file.url);
 | 
			
		||||
			os.success();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		addApp() {
 | 
			
		||||
			alert('not implemented yet');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteFile() {
 | 
			
		||||
			const { canceled } = await os.confirm({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			os.api('drive/files/delete', {
 | 
			
		||||
				fileId: this.file.id
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		bytes
 | 
			
		||||
	}
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	file: Misskey.entities.DriveFile;
 | 
			
		||||
	isSelected?: boolean;
 | 
			
		||||
	selectMode?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	isSelected: false,
 | 
			
		||||
	selectMode: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', r: Misskey.entities.DriveFile): void;
 | 
			
		||||
	(e: 'dragstart'): void;
 | 
			
		||||
	(e: 'dragend'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
 | 
			
		||||
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
 | 
			
		||||
 | 
			
		||||
function getMenu() {
 | 
			
		||||
	return [{
 | 
			
		||||
		text: i18n.locale.rename,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: rename
 | 
			
		||||
	}, {
 | 
			
		||||
		text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
 | 
			
		||||
		icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
 | 
			
		||||
		action: toggleSensitive
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.describeFile,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: describe
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.copyUrl,
 | 
			
		||||
		icon: 'fas fa-link',
 | 
			
		||||
		action: copyUrl
 | 
			
		||||
	}, {
 | 
			
		||||
		type: 'a',
 | 
			
		||||
		href: props.file.url,
 | 
			
		||||
		target: '_blank',
 | 
			
		||||
		text: i18n.locale.download,
 | 
			
		||||
		icon: 'fas fa-download',
 | 
			
		||||
		download: props.file.name
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.delete,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: deleteFile
 | 
			
		||||
	}];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onClick(ev: MouseEvent) {
 | 
			
		||||
	if (props.selectMode) {
 | 
			
		||||
		emit('chosen', props.file);
 | 
			
		||||
	} else {
 | 
			
		||||
		os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onContextmenu(e: MouseEvent) {
 | 
			
		||||
	os.contextMenu(getMenu(), e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragstart(e: DragEvent) {
 | 
			
		||||
	if (e.dataTransfer) {
 | 
			
		||||
		e.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
		e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
 | 
			
		||||
	}
 | 
			
		||||
	isDragging.value = true;
 | 
			
		||||
 | 
			
		||||
	emit('dragstart');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragend() {
 | 
			
		||||
	isDragging.value = false;
 | 
			
		||||
	emit('dragend');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rename() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.renameFile,
 | 
			
		||||
		placeholder: i18n.locale.inputNewFileName,
 | 
			
		||||
		default: props.file.name,
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
			fileId: props.file.id,
 | 
			
		||||
			name: name
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function describe() {
 | 
			
		||||
	os.popup(import('@/components/media-caption.vue'), {
 | 
			
		||||
		title: i18n.locale.describeFile,
 | 
			
		||||
		input: {
 | 
			
		||||
			placeholder: i18n.locale.inputNewDescription,
 | 
			
		||||
			default: props.file.comment !== null ? props.file.comment : '',
 | 
			
		||||
		},
 | 
			
		||||
		image: props.file
 | 
			
		||||
	}, {
 | 
			
		||||
		done: result => {
 | 
			
		||||
			if (!result || result.canceled) return;
 | 
			
		||||
			let comment = result.result;
 | 
			
		||||
			os.api('drive/files/update', {
 | 
			
		||||
				fileId: props.file.id,
 | 
			
		||||
				comment: comment.length == 0 ? null : comment
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}, 'closed');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSensitive() {
 | 
			
		||||
	os.api('drive/files/update', {
 | 
			
		||||
		fileId: props.file.id,
 | 
			
		||||
		isSensitive: !props.file.isSensitive
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function copyUrl() {
 | 
			
		||||
	copyToClipboard(props.file.url);
 | 
			
		||||
	os.success();
 | 
			
		||||
}
 | 
			
		||||
/*
 | 
			
		||||
function addApp() {
 | 
			
		||||
	alert('not implemented yet');
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
async function deleteFile() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	os.api('drive/files/delete', {
 | 
			
		||||
		fileId: props.file.id
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,243 +19,233 @@
 | 
			
		||||
		<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
 | 
			
		||||
		{{ folder.name }}
 | 
			
		||||
	</p>
 | 
			
		||||
	<p v-if="$store.state.uploadFolder == folder.id" class="upload">
 | 
			
		||||
		{{ $ts.uploadFolder }}
 | 
			
		||||
	<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
 | 
			
		||||
		{{ i18n.locale.uploadFolder }}
 | 
			
		||||
	</p>
 | 
			
		||||
	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		folder: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		isSelected: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
		selectMode: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['chosen'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			hover: false,
 | 
			
		||||
			draghover: false,
 | 
			
		||||
			isDragging: false,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		browser(): any {
 | 
			
		||||
			return this.$parent;
 | 
			
		||||
		},
 | 
			
		||||
		title(): string {
 | 
			
		||||
			return this.folder.name;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		checkboxClicked(e) {
 | 
			
		||||
			this.$emit('chosen', this.folder);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onClick() {
 | 
			
		||||
			this.browser.move(this.folder);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMouseover() {
 | 
			
		||||
			this.hover = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMouseout() {
 | 
			
		||||
			this.hover = false
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragover(e) {
 | 
			
		||||
			// 自分自身がドラッグされている場合
 | 
			
		||||
			if (this.isDragging) {
 | 
			
		||||
				// 自分自身にはドロップさせない
 | 
			
		||||
				e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
			if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
			} else {
 | 
			
		||||
				e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragenter() {
 | 
			
		||||
			if (!this.isDragging) this.draghover = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragleave() {
 | 
			
		||||
			this.draghover = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDrop(e) {
 | 
			
		||||
			this.draghover = false;
 | 
			
		||||
 | 
			
		||||
			// ファイルだったら
 | 
			
		||||
			if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
				for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
					this.browser.upload(file, this.folder);
 | 
			
		||||
				}
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのファイル
 | 
			
		||||
			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
			if (driveFile != null && driveFile != '') {
 | 
			
		||||
				const file = JSON.parse(driveFile);
 | 
			
		||||
				this.browser.removeFile(file.id);
 | 
			
		||||
				os.api('drive/files/update', {
 | 
			
		||||
					fileId: file.id,
 | 
			
		||||
					folderId: this.folder.id
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのフォルダ
 | 
			
		||||
			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
			if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
				const folder = JSON.parse(driveFolder);
 | 
			
		||||
 | 
			
		||||
				// 移動先が自分自身ならreject
 | 
			
		||||
				if (folder.id == this.folder.id) return;
 | 
			
		||||
 | 
			
		||||
				this.browser.removeFolder(folder.id);
 | 
			
		||||
				os.api('drive/folders/update', {
 | 
			
		||||
					folderId: folder.id,
 | 
			
		||||
					parentId: this.folder.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					// noop
 | 
			
		||||
				}).catch(err => {
 | 
			
		||||
					switch (err) {
 | 
			
		||||
						case 'detected-circular-definition':
 | 
			
		||||
							os.alert({
 | 
			
		||||
								title: this.$ts.unableToProcess,
 | 
			
		||||
								text: this.$ts.circularReferenceFolder
 | 
			
		||||
							});
 | 
			
		||||
							break;
 | 
			
		||||
						default:
 | 
			
		||||
							os.alert({
 | 
			
		||||
								type: 'error',
 | 
			
		||||
								text: this.$ts.somethingHappened
 | 
			
		||||
							});
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragstart(e) {
 | 
			
		||||
			e.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
 | 
			
		||||
			this.isDragging = true;
 | 
			
		||||
 | 
			
		||||
			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
 | 
			
		||||
			// (=あなたの子供が、ドラッグを開始しましたよ)
 | 
			
		||||
			this.browser.isDragSource = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragend() {
 | 
			
		||||
			this.isDragging = false;
 | 
			
		||||
			this.browser.isDragSource = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		go() {
 | 
			
		||||
			this.browser.move(this.folder.id);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		newWindow() {
 | 
			
		||||
			this.browser.newWindow(this.folder);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		rename() {
 | 
			
		||||
			os.inputText({
 | 
			
		||||
				title: this.$ts.renameFolder,
 | 
			
		||||
				placeholder: this.$ts.inputNewFolderName,
 | 
			
		||||
				default: this.folder.name
 | 
			
		||||
			}).then(({ canceled, result: name }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				os.api('drive/folders/update', {
 | 
			
		||||
					folderId: this.folder.id,
 | 
			
		||||
					name: name
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deleteFolder() {
 | 
			
		||||
			os.api('drive/folders/delete', {
 | 
			
		||||
				folderId: this.folder.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				if (this.$store.state.uploadFolder === this.folder.id) {
 | 
			
		||||
					this.$store.set('uploadFolder', null);
 | 
			
		||||
				}
 | 
			
		||||
			}).catch(err => {
 | 
			
		||||
				switch(err.id) {
 | 
			
		||||
					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 | 
			
		||||
						os.alert({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							title: this.$ts.unableToDelete,
 | 
			
		||||
							text: this.$ts.hasChildFilesOrFolders
 | 
			
		||||
						});
 | 
			
		||||
						break;
 | 
			
		||||
					default:
 | 
			
		||||
						os.alert({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							text: this.$ts.unableToDelete
 | 
			
		||||
						});
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		setAsUploadFolder() {
 | 
			
		||||
			this.$store.set('uploadFolder', this.folder.id);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onContextmenu(e) {
 | 
			
		||||
			os.contextMenu([{
 | 
			
		||||
				text: this.$ts.openInWindow,
 | 
			
		||||
				icon: 'fas fa-window-restore',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					os.popup(import('./drive-window.vue'), {
 | 
			
		||||
						initialFolder: this.folder
 | 
			
		||||
					}, {
 | 
			
		||||
					}, 'closed');
 | 
			
		||||
				}
 | 
			
		||||
			}, null, {
 | 
			
		||||
				text: this.$ts.rename,
 | 
			
		||||
				icon: 'fas fa-i-cursor',
 | 
			
		||||
				action: this.rename
 | 
			
		||||
			}, null, {
 | 
			
		||||
				text: this.$ts.delete,
 | 
			
		||||
				icon: 'fas fa-trash-alt',
 | 
			
		||||
				danger: true,
 | 
			
		||||
				action: this.deleteFolder
 | 
			
		||||
			}], e);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	folder: Misskey.entities.DriveFolder;
 | 
			
		||||
	isSelected?: boolean;
 | 
			
		||||
	selectMode?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	isSelected: false,
 | 
			
		||||
	selectMode: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const hover = ref(false);
 | 
			
		||||
const draghover = ref(false);
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
 | 
			
		||||
const title = computed(() => props.folder.name);
 | 
			
		||||
 | 
			
		||||
function checkboxClicked(e) {
 | 
			
		||||
	emit('chosen', props.folder);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onClick() {
 | 
			
		||||
	emit('move', props.folder);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onMouseover() {
 | 
			
		||||
	hover.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onMouseout() {
 | 
			
		||||
	hover.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragover(e: DragEvent) {
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// 自分自身がドラッグされている場合
 | 
			
		||||
	if (isDragging.value) {
 | 
			
		||||
		// 自分自身にはドロップさせない
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
	if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
	} else {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragenter() {
 | 
			
		||||
	if (!isDragging.value) draghover.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragleave() {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(e: DragEvent) {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ファイルだったら
 | 
			
		||||
	if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
			emit('upload', file, props.folder);
 | 
			
		||||
		}
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile != '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		emit('removeFile', file.id);
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
			fileId: file.id,
 | 
			
		||||
			folderId: props.folder.id
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのフォルダ
 | 
			
		||||
	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
		const folder = JSON.parse(driveFolder);
 | 
			
		||||
 | 
			
		||||
		// 移動先が自分自身ならreject
 | 
			
		||||
		if (folder.id == props.folder.id) return;
 | 
			
		||||
 | 
			
		||||
		emit('removeFolder', folder.id);
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
			folderId: folder.id,
 | 
			
		||||
			parentId: props.folder.id
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			// noop
 | 
			
		||||
		}).catch(err => {
 | 
			
		||||
			switch (err) {
 | 
			
		||||
				case 'detected-circular-definition':
 | 
			
		||||
					os.alert({
 | 
			
		||||
						title: i18n.locale.unableToProcess,
 | 
			
		||||
						text: i18n.locale.circularReferenceFolder
 | 
			
		||||
					});
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					os.alert({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: i18n.locale.somethingHappened
 | 
			
		||||
					});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragstart(e: DragEvent) {
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	e.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
	e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
 | 
			
		||||
	isDragging.value = true;
 | 
			
		||||
 | 
			
		||||
	// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
 | 
			
		||||
	// (=あなたの子供が、ドラッグを開始しましたよ)
 | 
			
		||||
	emit('dragstart');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragend() {
 | 
			
		||||
	isDragging.value = false;
 | 
			
		||||
	emit('dragend');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function go() {
 | 
			
		||||
	emit('move', props.folder.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rename() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.renameFolder,
 | 
			
		||||
		placeholder: i18n.locale.inputNewFolderName,
 | 
			
		||||
		default: props.folder.name
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
			folderId: props.folder.id,
 | 
			
		||||
			name: name
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteFolder() {
 | 
			
		||||
	os.api('drive/folders/delete', {
 | 
			
		||||
		folderId: props.folder.id
 | 
			
		||||
	}).then(() => {
 | 
			
		||||
		if (defaultStore.state.uploadFolder === props.folder.id) {
 | 
			
		||||
			defaultStore.set('uploadFolder', null);
 | 
			
		||||
		}
 | 
			
		||||
	}).catch(err => {
 | 
			
		||||
		switch(err.id) {
 | 
			
		||||
			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					title: i18n.locale.unableToDelete,
 | 
			
		||||
					text: i18n.locale.hasChildFilesOrFolders
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: i18n.locale.unableToDelete
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setAsUploadFolder() {
 | 
			
		||||
	defaultStore.set('uploadFolder', props.folder.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onContextmenu(e) {
 | 
			
		||||
	os.contextMenu([{
 | 
			
		||||
		text: i18n.locale.openInWindow,
 | 
			
		||||
		icon: 'fas fa-window-restore',
 | 
			
		||||
		action: () => {
 | 
			
		||||
			os.popup(import('./drive-window.vue'), {
 | 
			
		||||
				initialFolder: props.folder
 | 
			
		||||
			}, {
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		}
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.rename,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: rename,
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.delete,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: deleteFolder,
 | 
			
		||||
	}], e);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,114 +8,111 @@
 | 
			
		||||
	@drop.stop="onDrop"
 | 
			
		||||
>
 | 
			
		||||
	<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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		folder: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: false,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	folder?: Misskey.entities.DriveFolder;
 | 
			
		||||
	parentFolder: Misskey.entities.DriveFolder | null;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			hover: false,
 | 
			
		||||
			draghover: false,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'move', v?: Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
 | 
			
		||||
	(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
 | 
			
		||||
	(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		browser(): any {
 | 
			
		||||
			return this.$parent;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
const hover = ref(false);
 | 
			
		||||
const draghover = ref(false);
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onClick() {
 | 
			
		||||
			this.browser.move(this.folder);
 | 
			
		||||
		},
 | 
			
		||||
function onClick() {
 | 
			
		||||
	emit('move', props.folder);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		onMouseover() {
 | 
			
		||||
			this.hover = true;
 | 
			
		||||
		},
 | 
			
		||||
function onMouseover() {
 | 
			
		||||
	hover.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		onMouseout() {
 | 
			
		||||
			this.hover = false;
 | 
			
		||||
		},
 | 
			
		||||
function onMouseout() {
 | 
			
		||||
	hover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		onDragover(e) {
 | 
			
		||||
			// このフォルダがルートかつカレントディレクトリならドロップ禁止
 | 
			
		||||
			if (this.folder == null && this.browser.folder == null) {
 | 
			
		||||
				e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
			}
 | 
			
		||||
function onDragover(e: DragEvent) {
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
			const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
			if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
			} else {
 | 
			
		||||
				e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragenter() {
 | 
			
		||||
			if (this.folder || this.browser.folder) this.draghover = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDragleave() {
 | 
			
		||||
			if (this.folder || this.browser.folder) this.draghover = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onDrop(e) {
 | 
			
		||||
			this.draghover = false;
 | 
			
		||||
 | 
			
		||||
			// ファイルだったら
 | 
			
		||||
			if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
				for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
					this.browser.upload(file, this.folder);
 | 
			
		||||
				}
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのファイル
 | 
			
		||||
			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
			if (driveFile != null && driveFile != '') {
 | 
			
		||||
				const file = JSON.parse(driveFile);
 | 
			
		||||
				this.browser.removeFile(file.id);
 | 
			
		||||
				os.api('drive/files/update', {
 | 
			
		||||
					fileId: file.id,
 | 
			
		||||
					folderId: this.folder ? this.folder.id : null
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのフォルダ
 | 
			
		||||
			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
			if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
				const folder = JSON.parse(driveFolder);
 | 
			
		||||
				// 移動先が自分自身ならreject
 | 
			
		||||
				if (this.folder && folder.id == this.folder.id) return;
 | 
			
		||||
				this.browser.removeFolder(folder.id);
 | 
			
		||||
				os.api('drive/folders/update', {
 | 
			
		||||
					folderId: folder.id,
 | 
			
		||||
					parentId: this.folder ? this.folder.id : null
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
		}
 | 
			
		||||
	// このフォルダがルートかつカレントディレクトリならドロップ禁止
 | 
			
		||||
	if (props.folder == null && props.parentFolder == null) {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
	if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
	} else {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragenter() {
 | 
			
		||||
	if (props.folder || props.parentFolder) draghover.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragleave() {
 | 
			
		||||
	if (props.folder || props.parentFolder) draghover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(e: DragEvent) {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ファイルだったら
 | 
			
		||||
	if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
			emit('upload', file, props.folder);
 | 
			
		||||
		}
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile != '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		emit('removeFile', file.id);
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
			fileId: file.id,
 | 
			
		||||
			folderId: props.folder ? props.folder.id : null
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのフォルダ
 | 
			
		||||
	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
		const folder = JSON.parse(driveFolder);
 | 
			
		||||
		// 移動先が自分自身ならreject
 | 
			
		||||
		if (props.folder && folder.id == props.folder.id) return;
 | 
			
		||||
		emit('removeFolder', folder.id);
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
			folderId: folder.id,
 | 
			
		||||
			parentId: props.folder ? props.folder.id : null
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,58 +1,65 @@
 | 
			
		||||
<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')">
 | 
			
		||||
	<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
 | 
			
		||||
	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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import MkModal from '@/components/ui/modal.vue';
 | 
			
		||||
import MkEmojiPicker from '@/components/emoji-picker.vue';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkModal,
 | 
			
		||||
		MkEmojiPicker,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		manualShowing: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			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();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	manualShowing?: boolean;
 | 
			
		||||
	src?: HTMLElement;
 | 
			
		||||
	showPinned?: boolean;
 | 
			
		||||
	asReactionPicker?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	manualShowing: false,
 | 
			
		||||
	showPinned: true,
 | 
			
		||||
	asReactionPicker: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,50 +5,33 @@
 | 
			
		||||
	:can-resize="false"
 | 
			
		||||
	:mini="true"
 | 
			
		||||
	:front="true"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import MkWindow from '@/components/ui/window.vue';
 | 
			
		||||
import MkEmojiPicker from '@/components/emoji-picker.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkWindow,
 | 
			
		||||
		MkEmojiPicker,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	src?: HTMLElement;
 | 
			
		||||
	showPinned?: boolean;
 | 
			
		||||
	asReactionPicker?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	showPinned: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: any): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
function chosen(emoji: any) {
 | 
			
		||||
	emit('chosen', emoji);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
		<button v-for="emoji in emojis"
 | 
			
		||||
			:key="emoji"
 | 
			
		||||
			class="_button"
 | 
			
		||||
			@click="chosen(emoji, $event)"
 | 
			
		||||
			@click="emit('chosen', emoji, $event)"
 | 
			
		||||
		>
 | 
			
		||||
			<MkEmoji :emoji="emoji" :normal="true"/>
 | 
			
		||||
		</button>
 | 
			
		||||
@@ -15,35 +15,19 @@
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		emojis: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		initialShown: {
 | 
			
		||||
			required: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	emojis: string[];
 | 
			
		||||
	initialShown?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	emits: ['chosen'],
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: string, ev: MouseEvent): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			getStaticImageUrl,
 | 
			
		||||
			shown: this.initialShown,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		chosen(emoji: any, ev) {
 | 
			
		||||
			this.$parent.chosen(emoji, ev);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
const shown = ref(!!props.initialShown);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
 | 
			
		||||
	<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()">
 | 
			
		||||
<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="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
 | 
			
		||||
	<div ref="emojis" class="emojis">
 | 
			
		||||
		<section class="result">
 | 
			
		||||
			<div v-if="searchResultCustom.length > 0">
 | 
			
		||||
				<button v-for="emoji in searchResultCustom"
 | 
			
		||||
					:key="emoji"
 | 
			
		||||
					:key="emoji.id"
 | 
			
		||||
					class="_button"
 | 
			
		||||
					:title="emoji.name"
 | 
			
		||||
					tabindex="0"
 | 
			
		||||
					@click="chosen(emoji, $event)"
 | 
			
		||||
				>
 | 
			
		||||
					<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
 | 
			
		||||
					<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
 | 
			
		||||
					<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
 | 
			
		||||
					<img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="searchResultUnicode.length > 0">
 | 
			
		||||
@@ -43,9 +43,9 @@
 | 
			
		||||
			</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>
 | 
			
		||||
					<button v-for="emoji in $store.state.recentlyUsedEmojis"
 | 
			
		||||
					<button v-for="emoji in recentlyUsedEmojis"
 | 
			
		||||
						:key="emoji"
 | 
			
		||||
						class="_button"
 | 
			
		||||
						@click="chosen(emoji, $event)"
 | 
			
		||||
@@ -56,12 +56,12 @@
 | 
			
		||||
			</section>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<header class="_acrylic">{{ $ts.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>
 | 
			
		||||
			<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 + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<header class="_acrylic">{{ $ts.emoji }}</header>
 | 
			
		||||
			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.locale.emoji }}</header>
 | 
			
		||||
			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="tabs">
 | 
			
		||||
@@ -73,277 +73,272 @@
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import { emojilist } from '@/scripts/emojilist';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, computed, watch, onMounted } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
 | 
			
		||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 | 
			
		||||
import Ripple from '@/components/ripple.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { isTouchUsing } from '@/scripts/touch';
 | 
			
		||||
import { isMobile } from '@/scripts/is-mobile';
 | 
			
		||||
import { emojiCategories } from '@/instance';
 | 
			
		||||
import { emojiCategories, instance } from '@/instance';
 | 
			
		||||
import XSection from './emoji-picker.section.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XSection
 | 
			
		||||
	},
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	showPinned?: boolean;
 | 
			
		||||
	asReactionPicker?: boolean;
 | 
			
		||||
	maxHeight?: number;
 | 
			
		||||
	asDrawer?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	showPinned: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		showPinned: {
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: true,
 | 
			
		||||
		},
 | 
			
		||||
		asReactionPicker: {
 | 
			
		||||
			required: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxHeight: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false,
 | 
			
		||||
		},
 | 
			
		||||
		asDrawer: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: string): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	emits: ['chosen'],
 | 
			
		||||
const search = ref<HTMLInputElement>();
 | 
			
		||||
const emojis = ref<HTMLDivElement>();
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			emojilist: markRaw(emojilist),
 | 
			
		||||
			getStaticImageUrl,
 | 
			
		||||
			pinned: this.$store.reactiveState.reactions,
 | 
			
		||||
			width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
 | 
			
		||||
			height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
 | 
			
		||||
			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'],
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
const {
 | 
			
		||||
	reactions: pinned,
 | 
			
		||||
	reactionPickerWidth,
 | 
			
		||||
	reactionPickerHeight,
 | 
			
		||||
	disableShowingAnimatedImages,
 | 
			
		||||
	recentlyUsedEmojis,
 | 
			
		||||
} = defaultStore.reactiveState;
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		q() {
 | 
			
		||||
			this.$refs.emojis.scrollTop = 0;
 | 
			
		||||
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
 | 
			
		||||
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
 | 
			
		||||
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 === '') {
 | 
			
		||||
				this.searchResultCustom = [];
 | 
			
		||||
				this.searchResultUnicode = [];
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
watch(q, () => {
 | 
			
		||||
	if (emojis.value) emojis.value.scrollTop = 0;
 | 
			
		||||
 | 
			
		||||
			const q = this.q.replace(/:/g, '');
 | 
			
		||||
 | 
			
		||||
			const searchCustom = () => {
 | 
			
		||||
				const max = 8;
 | 
			
		||||
				const emojis = this.customEmojis;
 | 
			
		||||
				const matches = new Set();
 | 
			
		||||
 | 
			
		||||
				const exactMatch = emojis.find(e => e.name === q);
 | 
			
		||||
				if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
				if (q.includes(' ')) { // AND検索
 | 
			
		||||
					const keywords = q.split(' ');
 | 
			
		||||
 | 
			
		||||
					// 名前にキーワードが含まれている
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (keywords.every(keyword => emoji.name.includes(keyword))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					// 名前またはエイリアスにキーワードが含まれている
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.name.startsWith(q)) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.aliases.some(alias => alias.startsWith(q))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.name.includes(q)) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.aliases.some(alias => alias.includes(q))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return matches;
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const searchUnicode = () => {
 | 
			
		||||
				const max = 8;
 | 
			
		||||
				const emojis = this.emojilist;
 | 
			
		||||
				const matches = new Set();
 | 
			
		||||
 | 
			
		||||
				const exactMatch = emojis.find(e => e.name === q);
 | 
			
		||||
				if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
				if (q.includes(' ')) { // AND検索
 | 
			
		||||
					const keywords = q.split(' ');
 | 
			
		||||
 | 
			
		||||
					// 名前にキーワードが含まれている
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (keywords.every(keyword => emoji.name.includes(keyword))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					// 名前またはエイリアスにキーワードが含まれている
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.name.startsWith(q)) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.name.includes(q)) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
					for (const emoji of emojis) {
 | 
			
		||||
						if (emoji.keywords.some(keyword => keyword.includes(q))) {
 | 
			
		||||
							matches.add(emoji);
 | 
			
		||||
							if (matches.size >= max) break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return matches;
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			this.searchResultCustom = Array.from(searchCustom());
 | 
			
		||||
			this.searchResultUnicode = Array.from(searchUnicode());
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.focus();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		focus() {
 | 
			
		||||
			if (!isMobile && !isTouchUsing) {
 | 
			
		||||
				this.$refs.search.focus({
 | 
			
		||||
					preventScroll: true
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reset() {
 | 
			
		||||
			this.$refs.emojis.scrollTop = 0;
 | 
			
		||||
			this.q = '';
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		getKey(emoji: any) {
 | 
			
		||||
			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		chosen(emoji: any, ev) {
 | 
			
		||||
			if (ev) {
 | 
			
		||||
				const el = ev.currentTarget || ev.target;
 | 
			
		||||
				const rect = el.getBoundingClientRect();
 | 
			
		||||
				const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
				const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
				os.popup(Ripple, { x, y }, {}, 'end');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const key = this.getKey(emoji);
 | 
			
		||||
			this.$emit('chosen', key);
 | 
			
		||||
 | 
			
		||||
			// 最近使った絵文字更新
 | 
			
		||||
			if (!this.pinned.includes(key)) {
 | 
			
		||||
				let recents = this.$store.state.recentlyUsedEmojis;
 | 
			
		||||
				recents = recents.filter((e: any) => e !== key);
 | 
			
		||||
				recents.unshift(key);
 | 
			
		||||
				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		paste(event) {
 | 
			
		||||
			const paste = (event.clipboardData || window.clipboardData).getData('text');
 | 
			
		||||
			if (this.done(paste)) {
 | 
			
		||||
				event.preventDefault();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		done(query) {
 | 
			
		||||
			if (query == null) query = this.q;
 | 
			
		||||
			if (query == null) return;
 | 
			
		||||
			const q = query.replace(/:/g, '');
 | 
			
		||||
			const exactMatchCustom = this.customEmojis.find(e => e.name === q);
 | 
			
		||||
			if (exactMatchCustom) {
 | 
			
		||||
				this.chosen(exactMatchCustom);
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
			const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
 | 
			
		||||
			if (exactMatchUnicode) {
 | 
			
		||||
				this.chosen(exactMatchUnicode);
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
			if (this.searchResultCustom.length > 0) {
 | 
			
		||||
				this.chosen(this.searchResultCustom[0]);
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
			if (this.searchResultUnicode.length > 0) {
 | 
			
		||||
				this.chosen(this.searchResultUnicode[0]);
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	if (q.value == null || q.value === '') {
 | 
			
		||||
		searchResultCustom.value = [];
 | 
			
		||||
		searchResultUnicode.value = [];
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const newQ = q.value.replace(/:/g, '');
 | 
			
		||||
 | 
			
		||||
	const searchCustom = () => {
 | 
			
		||||
		const max = 8;
 | 
			
		||||
		const emojis = customEmojis;
 | 
			
		||||
		const matches = new Set<Misskey.entities.CustomEmoji>();
 | 
			
		||||
 | 
			
		||||
		const exactMatch = emojis.find(e => e.name === newQ);
 | 
			
		||||
		if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
		if (newQ.includes(' ')) { // AND検索
 | 
			
		||||
			const keywords = newQ.split(' ');
 | 
			
		||||
 | 
			
		||||
			// 名前にキーワードが含まれている
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (keywords.every(keyword => emoji.name.includes(keyword))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			// 名前またはエイリアスにキーワードが含まれている
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.name.startsWith(newQ)) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.name.includes(newQ)) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.aliases.some(alias => alias.includes(newQ))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return matches;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const searchUnicode = () => {
 | 
			
		||||
		const max = 8;
 | 
			
		||||
		const emojis = emojilist;
 | 
			
		||||
		const matches = new Set<UnicodeEmojiDef>();
 | 
			
		||||
 | 
			
		||||
		const exactMatch = emojis.find(e => e.name === newQ);
 | 
			
		||||
		if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
		if (newQ.includes(' ')) { // AND検索
 | 
			
		||||
			const keywords = newQ.split(' ');
 | 
			
		||||
 | 
			
		||||
			// 名前にキーワードが含まれている
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (keywords.every(keyword => emoji.name.includes(keyword))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			// 名前またはエイリアスにキーワードが含まれている
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.name.startsWith(newQ)) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.name.includes(newQ)) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const emoji of emojis) {
 | 
			
		||||
				if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
 | 
			
		||||
					matches.add(emoji);
 | 
			
		||||
					if (matches.size >= max) break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return matches;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	searchResultCustom.value = Array.from(searchCustom());
 | 
			
		||||
	searchResultUnicode.value = Array.from(searchUnicode());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
	if (!isMobile && !isTouchUsing) {
 | 
			
		||||
		search.value?.focus({
 | 
			
		||||
			preventScroll: true
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reset() {
 | 
			
		||||
	if (emojis.value) emojis.value.scrollTop = 0;
 | 
			
		||||
	q.value = '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
 | 
			
		||||
	return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function chosen(emoji: any, ev?: MouseEvent) {
 | 
			
		||||
	const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
 | 
			
		||||
	if (el) {
 | 
			
		||||
		const rect = el.getBoundingClientRect();
 | 
			
		||||
		const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
		os.popup(Ripple, { x, y }, {}, 'end');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const key = getKey(emoji);
 | 
			
		||||
	emit('chosen', key);
 | 
			
		||||
 | 
			
		||||
	// 最近使った絵文字更新
 | 
			
		||||
	if (!pinned.value.includes(key)) {
 | 
			
		||||
		let recents = defaultStore.state.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((e: any) => e !== key);
 | 
			
		||||
		recents.unshift(key);
 | 
			
		||||
		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function paste(event: ClipboardEvent) {
 | 
			
		||||
	const paste = (event.clipboardData || window.clipboardData).getData('text');
 | 
			
		||||
	if (done(paste)) {
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function done(query?: any): boolean | void {
 | 
			
		||||
	if (query == null) query = q.value;
 | 
			
		||||
	if (query == null || typeof query !== 'string') return;
 | 
			
		||||
 | 
			
		||||
	const q2 = query.replace(/:/g, '');
 | 
			
		||||
	const exactMatchCustom = customEmojis.find(e => e.name === q2);
 | 
			
		||||
	if (exactMatchCustom) {
 | 
			
		||||
		chosen(exactMatchCustom);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
 | 
			
		||||
	if (exactMatchUnicode) {
 | 
			
		||||
		chosen(exactMatchUnicode);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	if (searchResultCustom.value.length > 0) {
 | 
			
		||||
		chosen(searchResultCustom.value[0]);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	if (searchResultUnicode.value.length > 0) {
 | 
			
		||||
		chosen(searchResultUnicode.value[0]);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	focus();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	focus,
 | 
			
		||||
	reset,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,15 @@
 | 
			
		||||
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
	},
 | 
			
		||||
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			meta: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		os.api('meta', { detail: true }).then(meta => {
 | 
			
		||||
			this.meta = meta;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
os.api('meta', { detail: true }).then(gotMeta => {
 | 
			
		||||
	meta.value = gotMeta;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,129 +6,110 @@
 | 
			
		||||
>
 | 
			
		||||
	<template v-if="!wait">
 | 
			
		||||
		<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 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 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 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 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 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>
 | 
			
		||||
</button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		user: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		full: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
		large: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	user: Misskey.entities.UserDetailed,
 | 
			
		||||
	full?: boolean,
 | 
			
		||||
	large?: boolean,
 | 
			
		||||
}>(), {
 | 
			
		||||
	full: false,
 | 
			
		||||
	large: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isFollowing: this.user.isFollowing,
 | 
			
		||||
			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
 | 
			
		||||
			wait: false,
 | 
			
		||||
			connection: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
const isFollowing = ref(props.user.isFollowing);
 | 
			
		||||
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
 | 
			
		||||
const wait = ref(false);
 | 
			
		||||
const connection = stream.useChannel('main');
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		// 渡されたユーザー情報が不完全な場合
 | 
			
		||||
		if (this.user.isFollowing == null) {
 | 
			
		||||
			os.api('users/show', {
 | 
			
		||||
				userId: this.user.id
 | 
			
		||||
			}).then(u => {
 | 
			
		||||
				this.isFollowing = u.isFollowing;
 | 
			
		||||
				this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
if (props.user.isFollowing == null) {
 | 
			
		||||
	os.api('users/show', {
 | 
			
		||||
		userId: props.user.id
 | 
			
		||||
	}).then(u => {
 | 
			
		||||
		isFollowing.value = u.isFollowing;
 | 
			
		||||
		hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection = markRaw(stream.useChannel('main'));
 | 
			
		||||
 | 
			
		||||
		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 {
 | 
			
		||||
				if (this.isFollowing) {
 | 
			
		||||
					const { canceled } = await os.confirm({
 | 
			
		||||
						type: 'warning',
 | 
			
		||||
						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					if (canceled) return;
 | 
			
		||||
 | 
			
		||||
					await os.api('following/delete', {
 | 
			
		||||
						userId: this.user.id
 | 
			
		||||
					});
 | 
			
		||||
				} else {
 | 
			
		||||
					if (this.hasPendingFollowRequestFromYou) {
 | 
			
		||||
						await os.api('following/requests/cancel', {
 | 
			
		||||
							userId: this.user.id
 | 
			
		||||
						});
 | 
			
		||||
					} else if (this.user.isLocked) {
 | 
			
		||||
						await os.api('following/create', {
 | 
			
		||||
							userId: this.user.id
 | 
			
		||||
						});
 | 
			
		||||
						this.hasPendingFollowRequestFromYou = true;
 | 
			
		||||
					} else {
 | 
			
		||||
						await os.api('following/create', {
 | 
			
		||||
							userId: this.user.id
 | 
			
		||||
						});
 | 
			
		||||
						this.hasPendingFollowRequestFromYou = true;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.error(e);
 | 
			
		||||
			} finally {
 | 
			
		||||
				this.wait = false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
 | 
			
		||||
	if (user.id == props.user.id) {
 | 
			
		||||
		isFollowing.value = user.isFollowing;
 | 
			
		||||
		hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onClick() {
 | 
			
		||||
	wait.value = true;
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		if (isFollowing.value) {
 | 
			
		||||
			const { canceled } = await os.confirm({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await os.api('following/delete', {
 | 
			
		||||
				userId: props.user.id
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			if (hasPendingFollowRequestFromYou.value) {
 | 
			
		||||
				await os.api('following/requests/cancel', {
 | 
			
		||||
					userId: props.user.id
 | 
			
		||||
				});
 | 
			
		||||
			} else if (props.user.isLocked) {
 | 
			
		||||
				await os.api('following/create', {
 | 
			
		||||
					userId: props.user.id
 | 
			
		||||
				});
 | 
			
		||||
				hasPendingFollowRequestFromYou.value = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				await os.api('following/create', {
 | 
			
		||||
					userId: props.user.id
 | 
			
		||||
				});
 | 
			
		||||
				hasPendingFollowRequestFromYou.value = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(e);
 | 
			
		||||
	} finally {
 | 
			
		||||
		wait.value = false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	connection.on('follow', onFollowChange);
 | 
			
		||||
	connection.on('unfollow', onFollowChange);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	connection.dispose();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,72 +2,64 @@
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	:height="400"
 | 
			
		||||
	@close="$refs.dialog.close()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
	@close="dialog.close()"
 | 
			
		||||
	@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">
 | 
			
		||||
			<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>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
 | 
			
		||||
			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
 | 
			
		||||
				<template #label>{{ $ts.emailAddress }}</template>
 | 
			
		||||
				<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
 | 
			
		||||
				<template #label>{{ i18n.locale.emailAddress }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
 | 
			
		||||
			</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 class="sub">
 | 
			
		||||
			<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
 | 
			
		||||
			<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
 | 
			
		||||
		</div>
 | 
			
		||||
	</form>
 | 
			
		||||
	<div v-else>
 | 
			
		||||
		{{ $ts._forgotPassword.contactAdmin }}
 | 
			
		||||
	<div v-else class="bafecedb">
 | 
			
		||||
		{{ i18n.locale._forgotPassword.contactAdmin }}
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
	},
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'done'): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	emits: ['done', 'closed'],
 | 
			
		||||
let dialog: InstanceType<typeof XModalWindow> = $ref();
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			username: '',
 | 
			
		||||
			email: '',
 | 
			
		||||
			processing: false,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
let username = $ref('');
 | 
			
		||||
let email = $ref('');
 | 
			
		||||
let processing = $ref(false);
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async onSubmit() {
 | 
			
		||||
			this.processing = true;
 | 
			
		||||
			await os.apiWithDialog('request-reset-password', {
 | 
			
		||||
				username: this.username,
 | 
			
		||||
				email: this.email,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$emit('done');
 | 
			
		||||
			this.$refs.dialog.close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
async function onSubmit() {
 | 
			
		||||
	processing = true;
 | 
			
		||||
	await os.apiWithDialog('request-reset-password', {
 | 
			
		||||
		username,
 | 
			
		||||
		email,
 | 
			
		||||
	});
 | 
			
		||||
	emit('done');
 | 
			
		||||
	dialog.close();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@@ -81,4 +73,8 @@ export default defineComponent({
 | 
			
		||||
		padding: 24px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bafecedb {
 | 
			
		||||
	padding: 24px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -541,7 +541,7 @@ export const uploads = ref<{
 | 
			
		||||
	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;
 | 
			
		||||
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
 | 
			
		||||
export const emojilist = require('../emojilist.json') as {
 | 
			
		||||
export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
 | 
			
		||||
 | 
			
		||||
export type UnicodeEmojiDef = {
 | 
			
		||||
	name: string;
 | 
			
		||||
	keywords: 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[];
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user