refactor: Composition APIへ移行 (#8138)
* components/drive-file-thumbnail.vue * components/drive-select-dialog.vue * components/drive-window.vue * wip * wip drive.file.vue, drive.vue * fix prop * wip( * components/drive.folder.vue * maybe ok * ✌️ * fix variable * FIX FOLDER VARIABLE * components/emoji-picker-dialog.vue * Hate `$emit` * hate global property * components/emoji-picker-window.vue * components/emoji-picker.section.vue * fix * fixx * wip components/emoji-picker.vue * fix * defineExpose * ユニコード絵文字の型をもっといい感じに * components/featured-photos.vue * components/follow-button.vue * forgot-password.vue * forgot-password.vue * 🎨 * fix
This commit is contained in:
		
							parent
							
								
									efb0ffc4ec
								
							
						
					
					
						commit
						7be09a4af9
					
				
					 17 changed files with 1608 additions and 1763 deletions
				
			
		|  | @ -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[]; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue