enhance: メニュー関連をComposition API化、switchアイテム追加 (#8215)
* メニューをComposition API化、switchアイテム追加 クライアントサイド画像圧縮の準備 * メニュー型定義を分離 (TypeScriptの型支援が効かないので) * disabled * make keepOriginal to follow setting value * fix * fix * Fix * clean up
This commit is contained in:
		
							parent
							
								
									aa64ff6c94
								
							
						
					
					
						commit
						55b3ae22ee
					
				
					 10 changed files with 199 additions and 235 deletions
				
			
		|  | @ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?" | ||||||
| saved: "保存しました" | saved: "保存しました" | ||||||
| messaging: "チャット" | messaging: "チャット" | ||||||
| upload: "アップロード" | upload: "アップロード" | ||||||
|  | keepOriginalUploading: "オリジナル画像を保持" | ||||||
|  | keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" | ||||||
| fromDrive: "ドライブから" | fromDrive: "ドライブから" | ||||||
| fromUrl: "URLから" | fromUrl: "URLから" | ||||||
| uploadFromUrl: "URLアップロード" | uploadFromUrl: "URLアップロード" | ||||||
|  |  | ||||||
|  | @ -20,45 +20,33 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, ref, toRefs } from 'vue'; | import { toRefs, Ref } from 'vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import Ripple from '@/components/ripple.vue'; | import Ripple from '@/components/ripple.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	props: { | 	modelValue: boolean | Ref<boolean>; | ||||||
| 		modelValue: { | 	disabled?: boolean; | ||||||
| 			type: Boolean, | }>(); | ||||||
| 			default: false |  | ||||||
| 		}, |  | ||||||
| 		disabled: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: false |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	setup(props, context) { | const emit = defineEmits<{ | ||||||
| 		const button = ref<HTMLElement>(); | 	(e: 'update:modelValue', v: boolean): void; | ||||||
| 		const checked = toRefs(props).modelValue; | }>(); | ||||||
| 		const toggle = () => { |  | ||||||
| 			if (props.disabled) return; |  | ||||||
| 			context.emit('update:modelValue', !checked.value); |  | ||||||
| 
 | 
 | ||||||
| 			if (!checked.value) { | let button = $ref<HTMLElement>(); | ||||||
| 				const rect = button.value.getBoundingClientRect(); | const checked = toRefs(props).modelValue; | ||||||
| 				const x = rect.left + (button.value.offsetWidth / 2); | const toggle = () => { | ||||||
| 				const y = rect.top + (button.value.offsetHeight / 2); | 	if (props.disabled) return; | ||||||
| 				os.popup(Ripple, { x, y, particle: false }, {}, 'end'); | 	emit('update:modelValue', !checked.value); | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 
 | 
 | ||||||
| 		return { | 	if (!checked.value) { | ||||||
| 			button, | 		const rect = button.getBoundingClientRect(); | ||||||
| 			checked, | 		const x = rect.left + (button.offsetWidth / 2); | ||||||
| 			toggle, | 		const y = rect.top + (button.offsetHeight / 2); | ||||||
| 		}; | 		os.popup(Ripple, { x, y, particle: false }, {}, 'end'); | ||||||
| 	}, | 	} | ||||||
| }); | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,88 +1,71 @@ | ||||||
| <template> | <template> | ||||||
| <transition :name="$store.state.animation ? 'fade' : ''" appear> | <transition :name="$store.state.animation ? 'fade' : ''" appear> | ||||||
| 	<div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | 	<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | ||||||
| 		<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> | 		<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> | ||||||
| 	</div> | 	</div> | ||||||
| </transition> | </transition> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { onMounted, onBeforeUnmount } from 'vue'; | ||||||
| import contains from '@/scripts/contains'; | import contains from '@/scripts/contains'; | ||||||
| import MkMenu from './menu.vue'; | import MkMenu from './menu.vue'; | ||||||
|  | import { MenuItem } from './types/menu.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	items: MenuItem[]; | ||||||
| 		MkMenu, | 	ev: MouseEvent; | ||||||
| 	}, | }>(); | ||||||
| 	props: { |  | ||||||
| 		items: { |  | ||||||
| 			type: Array, |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		ev: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		viaKeyboard: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	emits: ['closed'], |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			zIndex: os.claimZIndex('high'), |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'esc': () => this.$emit('closed'), |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 |  | ||||||
| 		let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 |  | ||||||
| 
 | 
 | ||||||
| 		const width = this.$el.offsetWidth; | const emit = defineEmits<{ | ||||||
| 		const height = this.$el.offsetHeight; | 	(e: 'closed'): void; | ||||||
|  | }>(); | ||||||
| 
 | 
 | ||||||
| 		if (left + width - window.pageXOffset > window.innerWidth) { | let rootEl = $ref<HTMLDivElement>(); | ||||||
| 			left = window.innerWidth - width + window.pageXOffset; |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if (top + height - window.pageYOffset > window.innerHeight) { | let zIndex = $ref<number>(os.claimZIndex('high')); | ||||||
| 			top = window.innerHeight - height + window.pageYOffset; |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if (top < 0) { | onMounted(() => { | ||||||
| 			top = 0; | 	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||||
| 		} | 	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||||
| 
 | 
 | ||||||
| 		if (left < 0) { | 	const width = rootEl.offsetWidth; | ||||||
| 			left = 0; | 	const height = rootEl.offsetHeight; | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		this.$el.style.top = top + 'px'; | 	if (left + width - window.pageXOffset > window.innerWidth) { | ||||||
| 		this.$el.style.left = left + 'px'; | 		left = window.innerWidth - width + window.pageXOffset; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | 	if (top + height - window.pageYOffset > window.innerHeight) { | ||||||
| 			el.addEventListener('mousedown', this.onMousedown); | 		top = window.innerHeight - height + window.pageYOffset; | ||||||
| 		} | 	} | ||||||
| 	}, | 
 | ||||||
| 	beforeUnmount() { | 	if (top < 0) { | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | 		top = 0; | ||||||
| 			el.removeEventListener('mousedown', this.onMousedown); | 	} | ||||||
| 		} | 
 | ||||||
| 	}, | 	if (left < 0) { | ||||||
| 	methods: { | 		left = 0; | ||||||
| 		onMousedown(e) { | 	} | ||||||
| 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed'); | 
 | ||||||
| 		}, | 	rootEl.style.top = `${top}px`; | ||||||
|  | 	rootEl.style.left = `${left}px`; | ||||||
|  | 
 | ||||||
|  | 	for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||||
|  | 		el.addEventListener('mousedown', onMousedown); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  | 	for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||||
|  | 		el.removeEventListener('mousedown', onMousedown); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function onMousedown(e: Event) { | ||||||
|  | 	if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed'); | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="items" v-hotkey="keymap" | <div ref="itemsEl" v-hotkey="keymap" | ||||||
| 	class="rrevdjwt" | 	class="rrevdjwt" | ||||||
| 	:class="{ center: align === 'center', asDrawer }" | 	:class="{ center: align === 'center', asDrawer }" | ||||||
| 	:style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" | 	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" | ||||||
| 	@contextmenu.self="e => e.preventDefault()" | 	@contextmenu.self="e => e.preventDefault()" | ||||||
| > | > | ||||||
| 	<template v-for="(item, i) in items2"> | 	<template v-for="(item, i) in items2"> | ||||||
|  | @ -28,6 +28,9 @@ | ||||||
| 			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> | 			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> | ||||||
| 			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> | 			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> | ||||||
| 		</button> | 		</button> | ||||||
|  | 		<span v-else-if="item.type === 'switch'" :tabindex="i" class="item"> | ||||||
|  | 			<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> | ||||||
|  | 		</span> | ||||||
| 		<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> | 		<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> | ||||||
| 			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> | 			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> | ||||||
| 			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> | 			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> | ||||||
|  | @ -41,114 +44,78 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, ref, unref } from 'vue'; | import { nextTick, onMounted, watch } from 'vue'; | ||||||
| import { focusPrev, focusNext } from '@/scripts/focus'; | import { focusPrev, focusNext } from '@/scripts/focus'; | ||||||
| import contains from '@/scripts/contains'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
|  | import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	props: { | 	items: MenuItem[]; | ||||||
| 		items: { | 	viaKeyboard?: boolean; | ||||||
| 			type: Array, | 	asDrawer?: boolean; | ||||||
| 			required: true | 	align?: 'center' | string; | ||||||
| 		}, | 	width?: number; | ||||||
| 		viaKeyboard: { | 	maxHeight?: number; | ||||||
| 			type: Boolean, | }>(); | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		asDrawer: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		align: { |  | ||||||
| 			type: String, |  | ||||||
| 			requried: false |  | ||||||
| 		}, |  | ||||||
| 		width: { |  | ||||||
| 			type: Number, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		maxHeight: { |  | ||||||
| 			type: Number, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	emits: ['close'], |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			items2: [], |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'up|k|shift+tab': this.focusUp, |  | ||||||
| 				'down|j|tab': this.focusDown, |  | ||||||
| 				'esc': this.close, |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	watch: { |  | ||||||
| 		items: { |  | ||||||
| 			handler() { |  | ||||||
| 				const items = ref(unref(this.items).filter(item => item !== undefined)); |  | ||||||
| 
 | 
 | ||||||
| 				for (let i = 0; i < items.value.length; i++) { | const emit = defineEmits<{ | ||||||
| 					const item = items.value[i]; | 	(e: 'close'): void; | ||||||
| 					 | }>(); | ||||||
| 					if (item && item.then) { // if item is Promise |  | ||||||
| 						items.value[i] = { type: 'pending' }; |  | ||||||
| 						item.then(actualItem => { |  | ||||||
| 							items.value[i] = actualItem; |  | ||||||
| 						}); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 | 
 | ||||||
| 				this.items2 = items; | let itemsEl = $ref<HTMLDivElement>(); | ||||||
| 			}, | 
 | ||||||
| 			immediate: true | let items2: InnerMenuItem[] = $ref([]); | ||||||
| 		} | 
 | ||||||
| 	}, | let keymap = $computed(() => ({ | ||||||
| 	mounted() { | 	'up|k|shift+tab': focusUp, | ||||||
| 		if (this.viaKeyboard) { | 	'down|j|tab': focusDown, | ||||||
| 			this.$nextTick(() => { | 	'esc': close, | ||||||
| 				focusNext(this.$refs.items.children[0], true, false); | })); | ||||||
|  | 
 | ||||||
|  | watch(() => props.items, () => { | ||||||
|  | 	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); | ||||||
|  | 
 | ||||||
|  | 	for (let i = 0; i < items.length; i++) { | ||||||
|  | 		const item = items[i]; | ||||||
|  | 
 | ||||||
|  | 		if (item && 'then' in item) { // if item is Promise | ||||||
|  | 			items[i] = { type: 'pending' }; | ||||||
|  | 			item.then(actualItem => { | ||||||
|  | 				items2[i] = actualItem; | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		if (this.contextmenuEvent) { | 	items2 = items as InnerMenuItem[]; | ||||||
| 			this.$el.style.top = this.contextmenuEvent.pageY + 'px'; | }, { | ||||||
| 			this.$el.style.left = this.contextmenuEvent.pageX + 'px'; | 	immediate: true, | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| 			for (const el of Array.from(document.querySelectorAll('body *'))) { | onMounted(() => { | ||||||
| 				el.addEventListener('mousedown', this.onMousedown); | 	if (props.viaKeyboard) { | ||||||
| 			} | 		nextTick(() => { | ||||||
| 		} | 			focusNext(itemsEl.children[0], true, false); | ||||||
| 	}, | 		}); | ||||||
| 	beforeUnmount() { |  | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { |  | ||||||
| 			el.removeEventListener('mousedown', this.onMousedown); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		clicked(fn, ev) { |  | ||||||
| 			fn(ev); |  | ||||||
| 			this.close(); |  | ||||||
| 		}, |  | ||||||
| 		close() { |  | ||||||
| 			this.$emit('close'); |  | ||||||
| 		}, |  | ||||||
| 		focusUp() { |  | ||||||
| 			focusPrev(document.activeElement); |  | ||||||
| 		}, |  | ||||||
| 		focusDown() { |  | ||||||
| 			focusNext(document.activeElement); |  | ||||||
| 		}, |  | ||||||
| 		onMousedown(e) { |  | ||||||
| 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | function clicked(fn: MenuAction, ev: MouseEvent) { | ||||||
|  | 	fn(ev); | ||||||
|  | 	close(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function close() { | ||||||
|  | 	emit('close'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function focusUp() { | ||||||
|  | 	focusPrev(document.activeElement); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function focusDown() { | ||||||
|  | 	focusNext(document.activeElement); | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,44 +1,28 @@ | ||||||
| <template> | <template> | ||||||
| <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> | <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')"> | ||||||
| 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/> | 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> | ||||||
| </MkModal> | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { } from 'vue'; | ||||||
| import MkModal from './modal.vue'; | import MkModal from './modal.vue'; | ||||||
| import MkMenu from './menu.vue'; | import MkMenu from './menu.vue'; | ||||||
|  | import { MenuItem } from '@/types/menu'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | defineProps<{ | ||||||
| 	components: { | 	items: MenuItem[]; | ||||||
| 		MkModal, | 	align?: 'center' | string; | ||||||
| 		MkMenu, | 	width?: number; | ||||||
| 	}, | 	viaKeyboard?: boolean; | ||||||
|  | 	src?: any; | ||||||
|  | }>(); | ||||||
| 
 | 
 | ||||||
| 	props: { | const emit = defineEmits<{ | ||||||
| 		items: { | 	(e: 'closed'): void; | ||||||
| 			type: Array, | }>(); | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		align: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		width: { |  | ||||||
| 			type: Number, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		viaKeyboard: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		src: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	emits: ['close', 'closed'], | let modal = $ref<InstanceType<typeof MkModal>>(); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -7,8 +7,10 @@ import * as Misskey from 'misskey-js'; | ||||||
| import { apiUrl, url } from '@/config'; | import { apiUrl, url } from '@/config'; | ||||||
| import MkPostFormDialog from '@/components/post-form-dialog.vue'; | import MkPostFormDialog from '@/components/post-form-dialog.vue'; | ||||||
| import MkWaitingDialog from '@/components/waiting-dialog.vue'; | import MkWaitingDialog from '@/components/waiting-dialog.vue'; | ||||||
|  | import { MenuItem } from '@/types/menu'; | ||||||
| import { resolve } from '@/router'; | import { resolve } from '@/router'; | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
| 
 | 
 | ||||||
| export const pendingApiRequestsCount = ref(0); | export const pendingApiRequestsCount = ref(0); | ||||||
| 
 | 
 | ||||||
|  | @ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { | export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: { | ||||||
| 	align?: string; | 	align?: string; | ||||||
| 	width?: number; | 	width?: number; | ||||||
| 	viaKeyboard?: boolean; | 	viaKeyboard?: boolean; | ||||||
|  | @ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options? | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function contextMenu(items: any[], ev: MouseEvent) { | export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) { | ||||||
| 	ev.preventDefault(); | 	ev.preventDefault(); | ||||||
| 	return new Promise((resolve, reject) => { | 	return new Promise((resolve, reject) => { | ||||||
| 		let dispose; | 		let dispose; | ||||||
|  | @ -541,7 +543,7 @@ export const uploads = ref<{ | ||||||
| 	img: string; | 	img: string; | ||||||
| }[]>([]); | }[]>([]); | ||||||
| 
 | 
 | ||||||
| export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { | export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> { | ||||||
| 	if (folder && typeof folder == 'object') folder = folder.id; | 	if (folder && typeof folder == 'object') folder = folder.id; | ||||||
| 
 | 
 | ||||||
| 	return new Promise((resolve, reject) => { | 	return new Promise((resolve, reject) => { | ||||||
|  | @ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey | ||||||
| 
 | 
 | ||||||
| 			uploads.value.push(ctx); | 			uploads.value.push(ctx); | ||||||
| 
 | 
 | ||||||
|  | 			console.log(keepOriginal); | ||||||
|  | 
 | ||||||
| 			const data = new FormData(); | 			const data = new FormData(); | ||||||
| 			data.append('i', $i.token); | 			data.append('i', $i.token); | ||||||
| 			data.append('force', 'true'); | 			data.append('force', 'true'); | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ | ||||||
| 			<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> | 			<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> | ||||||
| 			<template #suffixIcon><i class="fas fa-folder-open"></i></template> | 			<template #suffixIcon><i class="fas fa-folder-open"></i></template> | ||||||
| 		</FormLink> | 		</FormLink> | ||||||
|  | 		<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch> | ||||||
| 	</FormSection> | 	</FormSection> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -36,18 +37,21 @@ | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as tinycolor from 'tinycolor2'; | import * as tinycolor from 'tinycolor2'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
|  | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import FormSplit from '@/components/form/split.vue'; | import FormSplit from '@/components/form/split.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as symbols from '@/symbols'; | import * as symbols from '@/symbols'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
| 
 | 
 | ||||||
| // TODO: render chart | // TODO: render chart | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		FormLink, | 		FormLink, | ||||||
|  | 		FormSwitch, | ||||||
| 		FormSection, | 		FormSection, | ||||||
| 		MkKeyValue, | 		MkKeyValue, | ||||||
| 		FormSplit, | 		FormSplit, | ||||||
|  | @ -79,7 +83,8 @@ export default defineComponent({ | ||||||
| 					l: 0.5 | 					l: 0.5 | ||||||
| 				}) | 				}) | ||||||
| 			}; | 			}; | ||||||
| 		} | 		}, | ||||||
|  | 		keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	async created() { | 	async created() { | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { ref } from 'vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | @ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities'; | ||||||
| 
 | 
 | ||||||
| function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { | function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { | ||||||
| 	return new Promise((res, rej) => { | 	return new Promise((res, rej) => { | ||||||
|  | 		const keepOriginal = ref(defaultStore.state.keepOriginalUploading); | ||||||
|  | 
 | ||||||
| 		const chooseFileFromPc = () => { | 		const chooseFileFromPc = () => { | ||||||
| 			const input = document.createElement('input'); | 			const input = document.createElement('input'); | ||||||
| 			input.type = 'file'; | 			input.type = 'file'; | ||||||
| 			input.multiple = multiple; | 			input.multiple = multiple; | ||||||
| 			input.onchange = () => { | 			input.onchange = () => { | ||||||
| 				const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); | 				const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); | ||||||
| 
 | 
 | ||||||
| 				Promise.all(promises).then(driveFiles => { | 				Promise.all(promises).then(driveFiles => { | ||||||
| 					res(multiple ? driveFiles : driveFiles[0]); | 					res(multiple ? driveFiles : driveFiles[0]); | ||||||
|  | @ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv | ||||||
| 			text: label, | 			text: label, | ||||||
| 			type: 'label' | 			type: 'label' | ||||||
| 		} : undefined, { | 		} : undefined, { | ||||||
|  | 			type: 'switch', | ||||||
|  | 			text: i18n.ts.keepOriginalUploading, | ||||||
|  | 			ref: keepOriginal | ||||||
|  | 		}, { | ||||||
| 			text: i18n.ts.upload, | 			text: i18n.ts.upload, | ||||||
| 			icon: 'fas fa-upload', | 			icon: 'fas fa-upload', | ||||||
| 			action: chooseFileFromPc | 			action: chooseFileFromPc | ||||||
|  |  | ||||||
|  | @ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', { | ||||||
| 		where: 'account', | 		where: 'account', | ||||||
| 		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]' | 		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]' | ||||||
| 	}, | 	}, | ||||||
|  | 	keepOriginalUploading: { | ||||||
|  | 		where: 'account', | ||||||
|  | 		default: false | ||||||
|  | 	}, | ||||||
| 	memo: { | 	memo: { | ||||||
| 		where: 'account', | 		where: 'account', | ||||||
| 		default: null | 		default: null | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								packages/client/src/types/menu.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/client/src/types/menu.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | import * as Misskey from 'misskey-js'; | ||||||
|  | import { Ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | export type MenuAction = (ev: MouseEvent) => void; | ||||||
|  | 
 | ||||||
|  | export type MenuDivider = null; | ||||||
|  | export type MenuNull = undefined; | ||||||
|  | export type MenuLabel = { type: 'label', text: string }; | ||||||
|  | export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; | ||||||
|  | export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; | ||||||
|  | export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; | ||||||
|  | export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean }; | ||||||
|  | export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; | ||||||
|  | 
 | ||||||
|  | export type MenuPending = { type: 'pending' }; | ||||||
|  | 
 | ||||||
|  | type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; | ||||||
|  | type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>; | ||||||
|  | export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; | ||||||
|  | export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue