Skip to content

Commit

Permalink
More usability improvements
Browse files Browse the repository at this point in the history
- Replaced all input tag based image selection with the IPC openFile based solution.
- Replaced showOpenDialog with openFile.
- Fixed the input dialog.
- Removed the now unused file upload dialog.
  • Loading branch information
matthew-holder-revvity committed Aug 30, 2024
1 parent 3e41ec6 commit 13c970c
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 237 deletions.
6 changes: 3 additions & 3 deletions src/main/system/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const useSystem = memo(() => {
await powerOffByDbus(interactive)
}

const showOpenDialog = ipcHandle(async (ev, options: OpenDialogOptions) => {
const openFile = ipcHandle(async (ev, options: OpenDialogOptions) => {
const result = await dialog.showOpenDialog(
BrowserWindow.fromWebContents(ev.sender) ?? raiseError(() => new ReferenceError('Invalid window')),
options
Expand Down Expand Up @@ -61,12 +61,12 @@ const useSystem = memo(() => {
})

ipcMain.handle('system:powerOff', ipcProxy(powerOff))
ipcMain.handle('system:showOpenDialog', showOpenDialog)
ipcMain.handle('system:openFile', openFile)
ipcMain.handle('system:saveFile', saveFile)

return {
powerOff
} satisfies Omit<SystemApi, 'showOpenDialog' | 'saveFile'>
} satisfies Omit<SystemApi, 'openFile' | 'saveFile'>
})

export default useSystem
2 changes: 1 addition & 1 deletion src/preload/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export interface SystemApi {
/** Powers off the system. */
readonly powerOff: (interactive?: boolean) => Promise<void>
/** Shows the open file dialog. */
readonly showOpenDialog: (options: OpenDialogOptions) => Promise<File[] | null>
readonly openFile: (options: OpenDialogOptions) => Promise<File[] | null>
/** Shows the save file dialog to save a file. */
readonly saveFile: (file: File, options: SaveDialogOptions) => Promise<boolean>
}
Expand Down
8 changes: 4 additions & 4 deletions src/preload/plugins/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ type SaveFileMain = (file: FileData, options: SaveDialogOptions) => Promise<bool
const useSystemApi = () => {
const ipc = useIpc()

const showOpenDialogMain: ShowOpenDialogMain = ipc.useInvoke('system:showOpenDialog')
async function showOpenDialog(options: OpenDialogOptions) {
const files = await showOpenDialogMain(options)
const openFileMain: ShowOpenDialogMain = ipc.useInvoke('system:openFile')
async function openFile(options: OpenDialogOptions) {
const files = await openFileMain(options)
if (files == null) return null

return files.map(({ path, buffer, type }) => new File([buffer], basename(path), { type }))
Expand All @@ -31,7 +31,7 @@ const useSystemApi = () => {

return {
powerOff: ipc.useInvoke('system:powerOff'),
showOpenDialog,
openFile,
saveFile
} satisfies SystemApi
}
Expand Down
135 changes: 0 additions & 135 deletions src/renderer/components/FileUploadDialog.vue

This file was deleted.

4 changes: 2 additions & 2 deletions src/renderer/components/InputDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const { t } = useI18n<I18nSchema>()
const confirmLabel = computed(() => props.confirmButton ?? t('common.confirm'))
const cancelLabel = computed(() => props.cancelButton ?? t('common.cancel'))
const isVisible = useVModel(props, 'visible')
const isVisible = useVModel(props, 'visible', emit, { passive: true, defaultValue: false })
// HACK: Always use cancelChanges since updateValue will dismiss the dialog after updating the original.
watch(isVisible, (value) => {
Expand Down Expand Up @@ -129,7 +129,7 @@ const disableConfirm = computed(() => props.mandatory && !is.nonEmptyString(inne

<template>
<VDialog v-model="isVisible" v-bind="dialogProps">
<template #activator="scope"><slot name="activator" v-bind="scope"></slot></template>
<template #activator="scope"><slot name="activator" v-bind="scope" /></template>
<VCard>
<VCardText>
<div v-if="is.nonEmptyString(title)">{{ title }}</div>
Expand Down
17 changes: 5 additions & 12 deletions src/renderer/components/OptionDialog.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import is from '@sindresorhus/is'
import { useVModel } from '@vueuse/core'
import { get } from 'radash'
import { watch, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
Expand All @@ -15,10 +16,11 @@ interface Props {
activatorProps?: object | undefined
attach?: string | boolean | Element | undefined
// State
// eslint-disable-next-line vue/no-unused-properties -- useVModel
visible?: boolean
disabled?: boolean
persistent?: boolean
scrollable?: boolean
visible?: boolean
// Size and location
absolute?: boolean
fullscreen?: boolean
Expand Down Expand Up @@ -64,21 +66,12 @@ const confirmLabel = computed(() => props.confirmButton ?? t('common.confirm'))
const showCancel = computed(() => props.persistent || props.withCancel)
const cancelLabel = computed(() => props.cancelButton ?? t('common.cancel'))
// eslint-disable-next-line vue/no-setup-props-reactivity-loss -- Watched and updated.
const isVisible = ref(props.visible)
watch(
() => props.visible,
(value) => {
isVisible.value = value
}
)
const isVisible = useVModel(props, 'visible', emit, { passive: true, defaultValue: false })
// HACK: Always use cancelChanges since updateValue will dismiss the dialog after updating the original.
watch(isVisible, (value) => {
if (!value) cancelChange()
})
watch(isVisible, (value) => {
if (value !== props.visible) emit('update:visible', value)
})
// eslint-disable-next-line vue/no-setup-props-reactivity-loss -- Watched and update.
const innerValue = ref(props.multiple ? toArray(props.modelValue) : props.modelValue)
Expand Down
85 changes: 85 additions & 0 deletions src/renderer/components/ReplacableImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { mdiFileImagePlus, mdiVideoInputHdmi } from '@mdi/js'
import videoInputHdmiIcon from '@mdi/svg/svg/video-input-hdmi.svg'
import { useObjectUrl } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
image?: File | undefined
}>()
const emit = defineEmits<{
(on: 'update', file: File): void
(on: 'change', file: File): void
}>()
const { t } = useI18n()
const file = computed(() => props.image)
const url = useObjectUrl(file)
async function selectImage() {
const results = await globalThis.services.system.openFile({
title: t('label.select'),
filters: [
{ extensions: ['svg', 'png', 'gif', 'jpg', 'jpeg'], name: t('filter.all') },
{ extensions: ['jpg', 'jpeg'], name: t('filter.jpg') },
{ extensions: ['svg'], name: t('filter.svg') },
{ extensions: ['png'], name: t('filter.png') },
{ extensions: ['gif'], name: t('filter.gif') }
]
})
const newFile = results?.at(0) ?? null
if (newFile != null) {
emit('update', newFile)
emit('change', newFile)
}
}
</script>

<template>
<VHover v-slot="{ isHovering, props: hover }">
<VAvatar
id="replacableImage"
size="128"
v-bind="{ ...hover }"
color="surface-lighten-1"
rounded="lg"
tile
@click="selectImage">
<VImg v-if="url != null" width="128" :src="url" :lazy-src="videoInputHdmiIcon" :cover="false" />
<VIcon v-else :icon="mdiVideoInputHdmi" />
<VOverlay
class="align-center justify-center text-center"
:model-value="isHovering ?? false"
theme="light"
scrim="primary-darken-4"
contained>
<VIcon :icon="mdiFileImagePlus" />
<div>{{ t('action.select') }}</div>
</VOverlay>
</VAvatar>
</VHover>
</template>

<style lang="scss" scoped>
#replacableImage {
cursor: pointer;
}
</style>

<i18n lang="yaml">
en:
action:
select: Select new image
label:
select: Select image
filter:
all: Support images
svg: SVG images
png: PNG images
gif: GIF images
jpg: JPEG Images
</i18n>
31 changes: 6 additions & 25 deletions src/renderer/modals/SourceDialog.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { mdiCamera, mdiClose, mdiVideoInputHdmi } from '@mdi/js'
import videoInputHdmiIcon from '@mdi/svg/svg/video-input-hdmi.svg'
import { useObjectUrl, useVModel } from '@vueuse/core'
import { computed, reactive, ref, watch } from 'vue'
import { mdiClose } from '@mdi/js'
import { useVModel } from '@vueuse/core'
import { reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ReplacableImage from '../components/ReplacableImage.vue'
import { useRules, useValidation } from '../helpers/validation'
import { useSources } from '../system/source'
import { useDialogs, useSourceDialog } from './dialogs'
Expand All @@ -29,9 +29,7 @@ const { t } = useI18n<I18nSchema>()
const sources = useSources()
const source = ref<NewSource>(sources.blank())
const files = ref<File[]>([])
const file = computed(() => files.value[0])
const image = useObjectUrl(file)
const file = ref<File>()
watch(file, (value) => {
v$.image.$model = value?.name ?? null
})
Expand Down Expand Up @@ -105,25 +103,8 @@ const { cardProps, isFullscreen } = useSourceDialog()
:label="t('label.name')"
:placeholder="t('placeholder.required')"
v-bind="getStatus(v$.title)" />
<VFileInput
v-model="files"
:label="t('label.image')"
accept="image/*"
prepend-icon=""
:placeholder="t('placeholder.required')"
:prepend-inner-icon="mdiCamera"
v-bind="getStatus(v$.title)" />
<div class="d-flex justify-center">
<VCard variant="outlined" max-width="128px" rounded="lg">
<VIcon v-if="image == null" size="128px" :icon="mdiVideoInputHdmi" />
<VImg
v-else
width="128px"
max-height="128px"
aspect-ratio="1/1"
:src="image"
:lazy-src="videoInputHdmiIcon" />
</VCard>
<ReplacableImage :image="file" @update="file = $event" />
</div>
</VForm>
</VCardText>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/pages/SettingsBackupPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function exportToFile() {
async function importFromFile() {
try {
const files = await globalThis.services.system.showOpenDialog({
const files = await globalThis.services.system.openFile({
title: t('label.import'),
filters: kFilters,
properties: ['openFile', 'dontAddToRecent']
Expand Down
Loading

0 comments on commit 13c970c

Please sign in to comment.