mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
Add desktop dialogs framework (#5605)
### Summary Adds desktop dialog framework with data-driven dialog definitions. ### Changes - Data-driven dialog structure in `desktopDialogs.ts` - Dynamic dialog view component with i18n support - Button action types: openUrl, close, cancel - Button severity levels for styling (primary, secondary, danger, warn) - Fallback invalid dialog for error handling - i18n collection script updated for dialog strings
This commit is contained in:
@@ -2,6 +2,7 @@ import * as fs from 'fs'
|
|||||||
|
|
||||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||||
|
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
||||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||||
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
||||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||||
@@ -131,6 +132,23 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Desktop Dialogs
|
||||||
|
const allDesktopDialogsLocale = Object.fromEntries(
|
||||||
|
Object.values(DESKTOP_DIALOGS).map((dialog) => [
|
||||||
|
normalizeI18nKey(dialog.id),
|
||||||
|
{
|
||||||
|
title: dialog.title,
|
||||||
|
message: dialog.message,
|
||||||
|
buttons: Object.fromEntries(
|
||||||
|
dialog.buttons.map((button) => [
|
||||||
|
normalizeI18nKey(button.label),
|
||||||
|
button.label
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
localePath,
|
localePath,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -144,7 +162,8 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
|||||||
...allSettingCategoriesLocale
|
...allSettingCategoriesLocale
|
||||||
},
|
},
|
||||||
serverConfigItems: allServerConfigsLocale,
|
serverConfigItems: allServerConfigsLocale,
|
||||||
serverConfigCategories: allServerConfigCategoriesLocale
|
serverConfigCategories: allServerConfigCategoriesLocale,
|
||||||
|
desktopDialogs: allDesktopDialogsLocale
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
--color-charcoal-700: #202121;
|
--color-charcoal-700: #202121;
|
||||||
--color-charcoal-800: #171718;
|
--color-charcoal-800: #171718;
|
||||||
|
|
||||||
|
--color-neutral-550: #636363;
|
||||||
|
|
||||||
--color-stone-100: #444444;
|
--color-stone-100: #444444;
|
||||||
--color-stone-200: #828282;
|
--color-stone-200: #828282;
|
||||||
--color-stone-300: #bbbbbb;
|
--color-stone-300: #bbbbbb;
|
||||||
@@ -103,6 +105,10 @@
|
|||||||
--color-danger-100: #c02323;
|
--color-danger-100: #c02323;
|
||||||
--color-danger-200: #d62952;
|
--color-danger-200: #d62952;
|
||||||
|
|
||||||
|
--color-coral-red-600: #973a40;
|
||||||
|
--color-coral-red-500: #c53f49;
|
||||||
|
--color-coral-red-400: #dd424e;
|
||||||
|
|
||||||
--color-bypass: #6a246a;
|
--color-bypass: #6a246a;
|
||||||
--color-error: #962a2a;
|
--color-error: #962a2a;
|
||||||
|
|
||||||
|
|||||||
75
src/constants/desktopDialogs.ts
Normal file
75
src/constants/desktopDialogs.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export interface DialogAction {
|
||||||
|
readonly label: string
|
||||||
|
readonly action: 'openUrl' | 'close' | 'cancel'
|
||||||
|
readonly url?: string
|
||||||
|
readonly severity?: 'danger' | 'primary' | 'secondary' | 'warn'
|
||||||
|
readonly returnValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopDialog {
|
||||||
|
readonly title: string
|
||||||
|
readonly message: string
|
||||||
|
readonly buttons: DialogAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DESKTOP_DIALOGS = {
|
||||||
|
/** Shown when a corrupt venv is detected. */
|
||||||
|
reinstallVenv: {
|
||||||
|
title: 'Reinstall ComfyUI (Fresh Start)?',
|
||||||
|
message: `Sorry, we can't launch ComfyUI because some installed packages aren't compatible.
|
||||||
|
|
||||||
|
Click Reinstall to restore ComfyUI and get back up and running.
|
||||||
|
|
||||||
|
Please note: if you've added custom nodes, you'll need to reinstall them after this process.`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: 'Learn More',
|
||||||
|
action: 'openUrl',
|
||||||
|
url: 'https://docs.comfy.org',
|
||||||
|
returnValue: 'openDocs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reinstall',
|
||||||
|
action: 'close',
|
||||||
|
severity: 'danger',
|
||||||
|
returnValue: 'resetVenv'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/** A dialog that is shown when an invalid dialog ID is provided. */
|
||||||
|
invalidDialog: {
|
||||||
|
title: 'Invalid Dialog',
|
||||||
|
message: `Invalid dialog ID was provided.`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: 'Close',
|
||||||
|
action: 'cancel',
|
||||||
|
returnValue: 'cancel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} as const satisfies { [K: string]: DesktopDialog }
|
||||||
|
|
||||||
|
/** The ID of a desktop dialog. */
|
||||||
|
type DesktopDialogId = keyof typeof DESKTOP_DIALOGS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if {@link id} is a valid dialog ID.
|
||||||
|
* @param id The string to check
|
||||||
|
* @returns `true` if the ID is a valid dialog ID, otherwise `false`
|
||||||
|
*/
|
||||||
|
function isDialogId(id: unknown): id is DesktopDialogId {
|
||||||
|
return typeof id === 'string' && id in DESKTOP_DIALOGS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the dialog with the given ID.
|
||||||
|
* @param dialogId The ID of the dialog to get
|
||||||
|
* @returns The dialog with the given ID
|
||||||
|
*/
|
||||||
|
export function getDialog(
|
||||||
|
dialogId: string | string[]
|
||||||
|
): DesktopDialog & { id: DesktopDialogId } {
|
||||||
|
const id = isDialogId(dialogId) ? dialogId : 'invalidDialog'
|
||||||
|
return { id, ...structuredClone(DESKTOP_DIALOGS[id]) }
|
||||||
|
}
|
||||||
@@ -115,6 +115,12 @@ const router = createRouter({
|
|||||||
name: 'DesktopUpdateView',
|
name: 'DesktopUpdateView',
|
||||||
component: () => import('@/views/DesktopUpdateView.vue'),
|
component: () => import('@/views/DesktopUpdateView.vue'),
|
||||||
beforeEnter: guardElectronAccess
|
beforeEnter: guardElectronAccess
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'desktop-dialog/:dialogId',
|
||||||
|
name: 'DesktopDialogView',
|
||||||
|
component: () => import('@/views/DesktopDialogView.vue'),
|
||||||
|
beforeEnter: guardElectronAccess
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/views/DesktopDialogView.vue
Normal file
70
src/views/DesktopDialogView.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
|
||||||
|
<h1 class="font-inter font-semibold text-xl m-0 italic">
|
||||||
|
{{ t(`desktopDialogs.${id}.title`, title) }}
|
||||||
|
</h1>
|
||||||
|
<p class="whitespace-pre-wrap">
|
||||||
|
{{ t(`desktopDialogs.${id}.message`, message) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
<Button
|
||||||
|
v-for="button in buttons"
|
||||||
|
:key="button.label"
|
||||||
|
class="rounded-lg first:mr-auto"
|
||||||
|
:label="
|
||||||
|
t(
|
||||||
|
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
|
||||||
|
button.label
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:severity="button.severity ?? 'secondary'"
|
||||||
|
@click="handleButtonClick(button)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { id, title, message, buttons } = getDialog(route.params.dialogId)
|
||||||
|
|
||||||
|
const handleButtonClick = async (button: DialogAction) => {
|
||||||
|
await electronAPI().Dialog.clickButton(button.returnValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference '../assets/css/style.css';
|
||||||
|
|
||||||
|
.p-button-secondary {
|
||||||
|
@apply text-white border-none bg-neutral-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button-secondary:hover {
|
||||||
|
@apply bg-neutral-550;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button-secondary:active {
|
||||||
|
@apply bg-neutral-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button-danger {
|
||||||
|
@apply bg-coral-red-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button-danger:hover {
|
||||||
|
@apply bg-coral-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button-danger:active {
|
||||||
|
@apply bg-coral-red-400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user