refactor(command): use dialog (#352)
parent
f249087a95
commit
462e85dad0
1
app.vue
1
app.vue
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
setupPageHeader()
|
setupPageHeader()
|
||||||
await setupI18n()
|
await setupI18n()
|
||||||
|
provideGlobalCommands()
|
||||||
|
|
||||||
// We want to trigger rerendering the page when account changes
|
// We want to trigger rerendering the page when account changes
|
||||||
const key = computed(() => `${currentServer.value}:${currentUser.value?.account.id || ''}`)
|
const key = computed(() => `${currentServer.value}:${currentUser.value?.account.id || ''}`)
|
||||||
|
|
|
@ -1,31 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CommandScope, QueryIndexedCommand } from '@/composables/command'
|
import type { CommandScope, QueryIndexedCommand } from '@/composables/command'
|
||||||
|
|
||||||
const isMac = useIsMac()
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const registry = useCommandRegistry()
|
const registry = useCommandRegistry()
|
||||||
|
|
||||||
const inputEl = $ref<HTMLInputElement>()
|
const inputEl = $ref<HTMLInputElement>()
|
||||||
const resultEl = $ref<HTMLDivElement>()
|
const resultEl = $ref<HTMLDivElement>()
|
||||||
|
|
||||||
let show = $ref(false)
|
const scopes = $ref<CommandScope[]>([])
|
||||||
let scopes = $ref<CommandScope[]>([])
|
let input = $(commandPanelInput)
|
||||||
let input = $ref('')
|
|
||||||
|
|
||||||
// listen to ctrl+/ on windows/linux or cmd+/ on mac
|
onMounted(() => {
|
||||||
useEventListener('keydown', async (e: KeyboardEvent) => {
|
inputEl?.focus()
|
||||||
if (e.key === '/' && (isMac.value ? e.metaKey : e.ctrlKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
show = true
|
|
||||||
scopes = []
|
|
||||||
input = '>'
|
|
||||||
await nextTick()
|
|
||||||
inputEl?.focus()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
onKeyStroke('Escape', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
show = false
|
|
||||||
}, { target: document })
|
|
||||||
|
|
||||||
const commandMode = $computed(() => input.startsWith('>'))
|
const commandMode = $computed(() => input.startsWith('>'))
|
||||||
const result = $computed(() => commandMode
|
const result = $computed(() => commandMode
|
||||||
|
@ -42,7 +32,7 @@ const findItemEl = (index: number) =>
|
||||||
const onCommandActivate = (item: QueryIndexedCommand) => {
|
const onCommandActivate = (item: QueryIndexedCommand) => {
|
||||||
if (item.onActivate) {
|
if (item.onActivate) {
|
||||||
item.onActivate()
|
item.onActivate()
|
||||||
show = false
|
emit('close')
|
||||||
}
|
}
|
||||||
else if (item.onComplete) {
|
else if (item.onComplete) {
|
||||||
scopes.push(item.onComplete())
|
scopes.push(item.onComplete())
|
||||||
|
@ -137,117 +127,86 @@ const onKeyDown = (e: KeyboardEvent) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Overlay -->
|
<div class="flex flex-col w-50vw max-w-180 h-50vh max-h-120">
|
||||||
<Transition
|
<!-- Input -->
|
||||||
enter-active-class="transition duration-200 ease-out"
|
<label class="flex mx-3 my-1 items-center">
|
||||||
enter-from-class="transform opacity-0"
|
<div mx-1 i-ri:search-line />
|
||||||
enter-to-class="transform opacity-100"
|
|
||||||
leave-active-class="transition duration-100 ease-in"
|
|
||||||
leave-from-class="transform opacity-100"
|
|
||||||
leave-to-class="transform opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="show"
|
|
||||||
class="z-100 fixed inset-0 opacity-70 bg-base"
|
|
||||||
@click="show = false"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Panel -->
|
<div v-for="scope in scopes" :key="scope.id" class="flex items-center mx-1 gap-2">
|
||||||
<Transition
|
<div class="text-sm">{{ scope.display }}</div>
|
||||||
enter-active-class="transition duration-65 ease-out"
|
<span class="text-secondary">/</span>
|
||||||
enter-from-class="transform scale-95"
|
</div>
|
||||||
enter-to-class="transform scale-100"
|
|
||||||
leave-active-class="transition duration-100 ease-in"
|
<input
|
||||||
leave-from-class="transform scale-100"
|
ref="inputEl"
|
||||||
leave-to-class="transform scale-95"
|
v-model="input"
|
||||||
>
|
class="focus:outline-none flex-1 p-2 rounded bg-base"
|
||||||
<div v-if="show" class="z-100 fixed inset-0 grid place-items-center pointer-events-none">
|
placeholder="Search"
|
||||||
<div
|
@keydown="onKeyDown"
|
||||||
class="flex flex-col w-50vw h-50vh rounded-md bg-base shadow-lg pointer-events-auto"
|
|
||||||
border="1 base"
|
|
||||||
>
|
>
|
||||||
<!-- Input -->
|
|
||||||
<label class="flex mx-3 my-1 items-center">
|
|
||||||
<div mx-1 i-ri:search-line />
|
|
||||||
|
|
||||||
<div v-for="scope in scopes" :key="scope.id" class="flex items-center mx-1 gap-2">
|
<CommandKey name="Escape" />
|
||||||
<div class="text-sm">{{ scope.display }}</div>
|
</label>
|
||||||
<span class="text-secondary">/</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
<div class="w-full border-b-1 border-base" />
|
||||||
ref="inputEl"
|
|
||||||
v-model="input"
|
<!-- Results -->
|
||||||
class="focus:outline-none flex-1 p-2 rounded bg-base"
|
<div ref="resultEl" class="flex-1 mx-1 overflow-y-auto">
|
||||||
placeholder="Search"
|
<template v-for="[scope, group] in result.grouped" :key="scope">
|
||||||
@keydown="onKeyDown"
|
<div class="mt-2 px-2 py-1 text-sm text-secondary">
|
||||||
|
{{ scope }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="cmd in group" :key="cmd.index">
|
||||||
|
<div
|
||||||
|
class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10"
|
||||||
|
:class="{ 'bg-active': active === cmd.index }"
|
||||||
|
:data-index="cmd.index"
|
||||||
|
@click="onCommandActivate(cmd)"
|
||||||
>
|
>
|
||||||
|
<div v-if="cmd.icon" mr-2 :class="cmd.icon" />
|
||||||
|
|
||||||
<CommandKey name="Escape" />
|
<div class="flex-1 flex items-baseline gap-2">
|
||||||
</label>
|
<div :class="{ 'font-medium': active === cmd.index }">
|
||||||
|
{{ cmd.name }}
|
||||||
<div class="w-full border-b-1 border-base" />
|
</div>
|
||||||
|
<div v-if="cmd.description" class="text-xs text-secondary">
|
||||||
<!-- Results -->
|
{{ cmd.description }}
|
||||||
<div ref="resultEl" class="flex-1 mx-1 overflow-y-auto">
|
</div>
|
||||||
<template v-for="[scope, group] in result.grouped" :key="scope">
|
|
||||||
<div class="mt-2 px-2 py-1 text-sm text-secondary">
|
|
||||||
{{ scope }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="cmd in group" :key="cmd.index">
|
<div
|
||||||
<div
|
v-if="cmd.onComplete"
|
||||||
class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10"
|
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
|
||||||
:class="{ 'bg-active': active === cmd.index }"
|
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
|
||||||
:data-index="cmd.index"
|
>
|
||||||
@click="onCommandActivate(cmd)"
|
<div class="text-xs text-secondary">
|
||||||
>
|
{{ $t('command.complete') }}
|
||||||
<div v-if="cmd.icon" mr-2 :class="cmd.icon" />
|
|
||||||
|
|
||||||
<div class="flex-1 flex items-baseline gap-2">
|
|
||||||
<div :class="{ 'font-medium': active === cmd.index }">
|
|
||||||
{{ cmd.name }}
|
|
||||||
</div>
|
|
||||||
<div v-if="cmd.description" class="text-xs text-secondary">
|
|
||||||
{{ cmd.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="cmd.onComplete"
|
|
||||||
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
|
|
||||||
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
|
|
||||||
>
|
|
||||||
<div class="text-xs text-secondary">
|
|
||||||
{{ $t('command.complete') }}
|
|
||||||
</div>
|
|
||||||
<CommandKey name="Tab" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="cmd.onActivate"
|
|
||||||
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
|
|
||||||
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
|
|
||||||
>
|
|
||||||
<div class="text-xs text-secondary">
|
|
||||||
{{ $t('command.activate') }}
|
|
||||||
</div>
|
|
||||||
<CommandKey name="Enter" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<CommandKey name="Tab" />
|
||||||
</template>
|
</div>
|
||||||
</div>
|
<div
|
||||||
|
v-if="cmd.onActivate"
|
||||||
<div class="w-full border-b-1 border-base" />
|
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
|
||||||
|
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
|
||||||
<!-- Footer -->
|
>
|
||||||
<div class="flex items-center px-3 py-1 text-xs">
|
<div class="text-xs text-secondary">
|
||||||
<div i-ri:lightbulb-flash-line /> Tip: Use
|
{{ $t('command.activate') }}
|
||||||
<!-- <CommandKey name="Ctrl+K" /> to search, -->
|
</div>
|
||||||
<CommandKey name="Ctrl+/" /> to activate command mode.
|
<CommandKey name="Enter" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
|
||||||
|
<div class="w-full border-b-1 border-base" />
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center px-3 py-1 text-xs">
|
||||||
|
<div i-ri:lightbulb-flash-line /> Tip: Use
|
||||||
|
<!-- <CommandKey name="Ctrl+K" /> to search, -->
|
||||||
|
<CommandKey name="Ctrl+/" /> to activate command mode.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
provideGlobalCommands()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CommandPanel />
|
|
||||||
</template>
|
|
|
@ -1,11 +1,23 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
isCommandPanelOpen,
|
||||||
isEditHistoryDialogOpen,
|
isEditHistoryDialogOpen,
|
||||||
isMediaPreviewOpen,
|
isMediaPreviewOpen,
|
||||||
isPreviewHelpOpen,
|
isPreviewHelpOpen,
|
||||||
isPublishDialogOpen,
|
isPublishDialogOpen,
|
||||||
isSigninDialogOpen,
|
isSigninDialogOpen,
|
||||||
} from '~/composables/dialog'
|
} from '~/composables/dialog'
|
||||||
|
|
||||||
|
const isMac = useIsMac()
|
||||||
|
|
||||||
|
// TODO: temporary, await for keybind system
|
||||||
|
// listen to ctrl+/ on windows/linux or cmd+/ on mac
|
||||||
|
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === '/' && (isMac.value ? e.metaKey : e.ctrlKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
openCommandPanel(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -30,4 +42,7 @@ import {
|
||||||
<ModalDialog v-model="isEditHistoryDialogOpen">
|
<ModalDialog v-model="isEditHistoryDialogOpen">
|
||||||
<StatusEditPreview :edit="statusEdit" />
|
<StatusEditPreview :edit="statusEdit" />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
|
<ModalDialog v-model="isCommandPanelOpen" max-w-fit flex>
|
||||||
|
<CommandPanel @close="closeCommandPanel()" />
|
||||||
|
</ModalDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -281,7 +281,7 @@ export const provideGlobalCommands = () => {
|
||||||
visible: () => users.value.length > 1,
|
visible: () => users.value.length > 1,
|
||||||
|
|
||||||
name: () => t('action.switch_account'),
|
name: () => t('action.switch_account'),
|
||||||
description: t('command.switch_account_desc'),
|
description: () => t('command.switch_account_desc'),
|
||||||
icon: 'i-ri:user-shared-line',
|
icon: 'i-ri:user-shared-line',
|
||||||
|
|
||||||
onComplete: () => ({
|
onComplete: () => ({
|
||||||
|
|
|
@ -8,6 +8,8 @@ export const mediaPreviewIndex = ref(0)
|
||||||
export const statusEdit = ref<StatusEdit>()
|
export const statusEdit = ref<StatusEdit>()
|
||||||
export const dialogDraftKey = ref<string>()
|
export const dialogDraftKey = ref<string>()
|
||||||
|
|
||||||
|
export const commandPanelInput = ref('')
|
||||||
|
|
||||||
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock)
|
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock)
|
||||||
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
|
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
|
||||||
|
|
||||||
|
@ -16,6 +18,7 @@ export const isPublishDialogOpen = ref(false)
|
||||||
export const isMediaPreviewOpen = ref(false)
|
export const isMediaPreviewOpen = ref(false)
|
||||||
export const isEditHistoryDialogOpen = ref(false)
|
export const isEditHistoryDialogOpen = ref(false)
|
||||||
export const isPreviewHelpOpen = ref(isFirstVisit.value)
|
export const isPreviewHelpOpen = ref(isFirstVisit.value)
|
||||||
|
export const isCommandPanelOpen = ref(false)
|
||||||
|
|
||||||
export const toggleZenMode = useToggle(isZenMode)
|
export const toggleZenMode = useToggle(isZenMode)
|
||||||
|
|
||||||
|
@ -72,3 +75,12 @@ export function openPreviewHelp() {
|
||||||
export function closePreviewHelp() {
|
export function closePreviewHelp() {
|
||||||
isPreviewHelpOpen.value = false
|
isPreviewHelpOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openCommandPanel(isCommandMode = false) {
|
||||||
|
commandPanelInput.value = isCommandMode ? '>' : ''
|
||||||
|
isCommandPanelOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeCommandPanel() {
|
||||||
|
isCommandPanelOpen.value = false
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,5 @@
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<CommandRoot />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue