Compare commits
22 Commits
devtools/r
...
core/1.27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
799e2d5569 | ||
|
|
3ab97100d2 | ||
|
|
58773f51f7 | ||
|
|
feb3c078fa | ||
|
|
71a2eaa902 | ||
|
|
a310c3cd8e | ||
|
|
1a6ef57643 | ||
|
|
d3dc330d56 | ||
|
|
cdff3f90ce | ||
|
|
470663e25d | ||
|
|
884a9886e0 | ||
|
|
0ee3138485 | ||
|
|
d6f0cae35c | ||
|
|
a91948352d | ||
|
|
df3060b8e2 | ||
|
|
26d777deee | ||
|
|
77ca6d54a0 | ||
|
|
12f23dac4f | ||
|
|
dac73dc7ed | ||
|
|
a99df5d3dd | ||
|
|
909866fd1d | ||
|
|
c39a03f5fd |
15
.github/workflows/json-validate.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Validate JSON
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
json-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate JSON syntax
|
||||
run: ./scripts/cicd/check-json.sh
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -33,7 +33,16 @@ export default defineConfig([
|
||||
},
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
projectService: true,
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'scripts/collect-i18n-node-defs.ts'
|
||||
]
|
||||
},
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.27.5",
|
||||
"version": "1.27.10",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
headless: true
|
||||
},
|
||||
reporter: 'list',
|
||||
workers: 1,
|
||||
timeout: 60000,
|
||||
testMatch: /collect-i18n-.*\.ts/
|
||||
})
|
||||
|
||||
3
public/assets/images/comfy-brand-mark.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/assets/images/nvidia-logo-square.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
77
scripts/cicd/check-json.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--debug]" >&2
|
||||
}
|
||||
|
||||
debug=0
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--debug)
|
||||
debug=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Validate JSON syntax in tracked files using jq
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: jq is required but not installed" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
EXCLUDE_PATTERNS=(
|
||||
'**/tsconfig*.json'
|
||||
)
|
||||
|
||||
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
|
||||
# shellcheck disable=SC2206
|
||||
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
|
||||
fi
|
||||
|
||||
pathspecs=(-- '*.json')
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ ${pattern:0:1} == ':' ]]; then
|
||||
pathspecs+=("$pattern")
|
||||
else
|
||||
pathspecs+=(":(glob,exclude)${pattern}")
|
||||
fi
|
||||
done
|
||||
|
||||
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
|
||||
|
||||
if [ "${#json_files[@]}" -eq 0 ]; then
|
||||
echo 'No JSON files found.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$debug" -eq 1 ]; then
|
||||
echo 'JSON files to validate:'
|
||||
printf ' %s\n' "${json_files[@]}"
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for file in "${json_files[@]}"; do
|
||||
if ! jq -e . "$file" >/dev/null; then
|
||||
echo "Invalid JSON syntax: $file" >&2
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
echo 'JSON validation failed.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo 'All JSON files are valid.'
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
||||
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(
|
||||
localePath,
|
||||
JSON.stringify(
|
||||
@@ -144,7 +162,8 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
||||
...allSettingCategoriesLocale
|
||||
},
|
||||
serverConfigItems: allServerConfigsLocale,
|
||||
serverConfigCategories: allServerConfigCategoriesLocale
|
||||
serverConfigCategories: allServerConfigCategoriesLocale,
|
||||
desktopDialogs: allDesktopDialogsLocale
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import type { ComfyNodeDef } from '../src/schemas/nodeDefSchema'
|
||||
import type { ComfyApi } from '../src/scripts/api'
|
||||
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
@@ -11,23 +11,28 @@ const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||
|
||||
test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
// Mock view route
|
||||
comfyPage.page.route('**/view**', async (route) => {
|
||||
await comfyPage.page.route('**/view**', async (route) => {
|
||||
await route.fulfill({
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
})
|
||||
|
||||
const nodeDefs: ComfyNodeDefImpl[] = (
|
||||
Object.values(
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const api = window['app'].api as ComfyApi
|
||||
return await api.getNodeDefs()
|
||||
})
|
||||
) as ComfyNodeDef[]
|
||||
// Note: Don't mock the object_info API endpoint - let it hit the actual backend
|
||||
|
||||
const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
|
||||
async () => {
|
||||
const api = window['app'].api
|
||||
const rawNodeDefs = await api.getNodeDefs()
|
||||
const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')
|
||||
|
||||
return (
|
||||
Object.values(rawNodeDefs)
|
||||
// Ignore DevTools nodes (used for internal testing)
|
||||
.filter((def: ComfyNodeDef) => !def.name.startsWith('DevTools'))
|
||||
.map((def: ComfyNodeDef) => new ComfyNodeDefImpl(def))
|
||||
)
|
||||
}
|
||||
)
|
||||
// Ignore DevTools nodes (used for internal testing)
|
||||
.filter((def) => !def.name.startsWith('DevTools'))
|
||||
.map((def) => new ComfyNodeDefImpl(def))
|
||||
|
||||
console.log(`Collected ${nodeDefs.length} node definitions`)
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
--color-charcoal-700: #202121;
|
||||
--color-charcoal-800: #171718;
|
||||
|
||||
--color-neutral-550: #636363;
|
||||
|
||||
--color-stone-100: #444444;
|
||||
--color-stone-200: #828282;
|
||||
--color-stone-300: #bbbbbb;
|
||||
@@ -103,6 +105,10 @@
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
--color-coral-red-600: #973a40;
|
||||
--color-coral-red-500: #c53f49;
|
||||
--color-coral-red-400: #dd424e;
|
||||
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
||||
>
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div ref="terminalEl" class="h-full terminal-host" />
|
||||
</div>
|
||||
@@ -97,12 +100,13 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
:deep(.p-terminal) .xterm {
|
||||
overflow-x: auto;
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
@apply bg-neutral-900 overflow-hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
71
src/components/common/StartupDisplay.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div class="grid grid-rows-2 gap-8">
|
||||
<!-- Top container: Logo -->
|
||||
<div class="flex items-end justify-center">
|
||||
<img
|
||||
src="/assets/images/comfy-brand-mark.svg"
|
||||
:alt="t('g.logoAlt')"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
<!-- Bottom container: Progress and text -->
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<ProgressBar
|
||||
v-if="!hideProgress"
|
||||
:mode="progressMode"
|
||||
:value="progressPercentage ?? 0"
|
||||
:show-value="false"
|
||||
class="w-90 h-2 mt-8"
|
||||
:pt="{ value: { class: 'bg-brand-yellow' } }"
|
||||
/>
|
||||
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="statusText" class="text-lg text-neutral-400">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/** Props for the StartupDisplay component */
|
||||
interface StartupDisplayProps {
|
||||
/** Progress: 0-100 for determinate, undefined for indeterminate */
|
||||
progressPercentage?: number
|
||||
/** Main title text */
|
||||
title?: string
|
||||
/** Status text shown below the title */
|
||||
statusText?: string
|
||||
/** Hide the progress bar */
|
||||
hideProgress?: boolean
|
||||
/** Use full screen wrapper (default: true) */
|
||||
fullScreen?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
progressPercentage,
|
||||
title,
|
||||
statusText,
|
||||
hideProgress = false,
|
||||
fullScreen = true
|
||||
} = defineProps<StartupDisplayProps>()
|
||||
|
||||
const progressMode = computed(() =>
|
||||
progressPercentage === undefined ? 'indeterminate' : 'determinate'
|
||||
)
|
||||
|
||||
const wrapperClass = computed(() =>
|
||||
fullScreen
|
||||
? 'flex items-center justify-center min-h-screen'
|
||||
: 'flex items-center justify-center'
|
||||
)
|
||||
</script>
|
||||
@@ -138,7 +138,7 @@ const allMissingNodesInstalled = computed(() => {
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled) {
|
||||
if (allInstalled && showInstallAllButton.value) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -124,50 +124,43 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Set up event listeners only after the widget is mounted and visible
|
||||
const setupDOMEventListeners = () => {
|
||||
if (!isDOMWidget(widget) || !widgetState.visible) return
|
||||
|
||||
if (widget.element.blur) {
|
||||
useEventListener(document, 'mousedown', (event) => {
|
||||
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||
widget.element.blur()
|
||||
}
|
||||
})
|
||||
useEventListener(document, 'mousedown', (event) => {
|
||||
if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {
|
||||
return
|
||||
}
|
||||
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||
widget.element.blur()
|
||||
}
|
||||
})
|
||||
|
||||
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
|
||||
useEventListener(widget.element, evt, () => {
|
||||
onMounted(() => {
|
||||
if (!isDOMWidget(widget)) {
|
||||
return
|
||||
}
|
||||
useEventListener(
|
||||
widget.element,
|
||||
widget.options.selectOn ?? ['focus', 'click'],
|
||||
() => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
lgCanvas?.selectNode(widget.node)
|
||||
lgCanvas?.bringToFront(widget.node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Set up event listeners when widget becomes visible
|
||||
watch(
|
||||
() => widgetState.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
setupDOMEventListeners()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const inputSpec = widget.node.constructor.nodeData
|
||||
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||
|
||||
// Mount DOM element when widget is or becomes visible
|
||||
const mountElementIfVisible = () => {
|
||||
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
|
||||
// Only append if not already a child
|
||||
if (!widgetElement.value.contains(widget.element)) {
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {
|
||||
return
|
||||
}
|
||||
// Only append if not already a child
|
||||
if (widgetElement.value.contains(widget.element)) {
|
||||
return
|
||||
}
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
|
||||
// Check on mount - but only after next tick to ensure visibility is calculated
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg">
|
||||
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg text-sm">
|
||||
<!-- Auto Update Setting -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-medium text-neutral-100">
|
||||
{{ $t('install.settings.autoUpdate') }}
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-400 mt-1">
|
||||
<p class="text-neutral-400 mt-1">
|
||||
{{ $t('install.settings.autoUpdateDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -32,14 +32,10 @@
|
||||
<h3 class="text-lg font-medium text-neutral-100">
|
||||
{{ $t('install.settings.allowMetrics') }}
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-400 mt-1">
|
||||
<p class="text-neutral-400">
|
||||
{{ $t('install.settings.allowMetricsDescription') }}
|
||||
</p>
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm text-blue-400 hover:text-blue-300 mt-1 inline-block"
|
||||
@click.prevent="showMetricsInfo"
|
||||
>
|
||||
<a href="#" @click.prevent="showMetricsInfo">
|
||||
{{ $t('install.settings.learnMoreAboutData') }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -51,7 +47,9 @@
|
||||
<Dialog
|
||||
v-model:visible="showDialog"
|
||||
modal
|
||||
dismissable-mask
|
||||
:header="$t('install.settings.dataCollectionDialog.title')"
|
||||
class="select-none"
|
||||
>
|
||||
<div class="text-neutral-300">
|
||||
<h4 class="font-medium mb-2">
|
||||
@@ -110,11 +108,7 @@
|
||||
</ul>
|
||||
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
<a href="https://comfy.org/privacy" target="_blank">
|
||||
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,130 +1,66 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 w-[600px] h-[30rem] select-none">
|
||||
<!-- Installation Path Section -->
|
||||
<div class="grow flex flex-col gap-4 text-neutral-300">
|
||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||
{{ $t('install.gpuSelection.selectGpu') }}
|
||||
</h2>
|
||||
<div
|
||||
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
|
||||
>
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
{{ $t('install.gpuPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<p class="m-1 text-neutral-400">
|
||||
{{ $t('install.gpuSelection.selectGpuDescription') }}:
|
||||
</p>
|
||||
<!-- GPU Selection buttons - takes up remaining space and centers content -->
|
||||
<div class="flex-1 flex gap-8 justify-center items-center">
|
||||
<!-- Apple Metal / NVIDIA -->
|
||||
<HardwareOption
|
||||
v-if="platform === 'darwin'"
|
||||
:image-path="'assets/images/apple-mps-logo.png'"
|
||||
placeholder-text="Apple Metal"
|
||||
subtitle="Apple Metal"
|
||||
:value="'mps'"
|
||||
:selected="selected === 'mps'"
|
||||
:recommended="true"
|
||||
@click="pickGpu('mps')"
|
||||
/>
|
||||
<HardwareOption
|
||||
v-else
|
||||
:image-path="'assets/images/nvidia-logo-square.jpg'"
|
||||
placeholder-text="NVIDIA"
|
||||
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
|
||||
:value="'nvidia'"
|
||||
:selected="selected === 'nvidia'"
|
||||
:recommended="true"
|
||||
@click="pickGpu('nvidia')"
|
||||
/>
|
||||
<!-- CPU -->
|
||||
<HardwareOption
|
||||
placeholder-text="CPU"
|
||||
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
|
||||
:value="'cpu'"
|
||||
:selected="selected === 'cpu'"
|
||||
@click="pickGpu('cpu')"
|
||||
/>
|
||||
<!-- Manual Install -->
|
||||
<HardwareOption
|
||||
placeholder-text="Manual Install"
|
||||
:subtitle="$t('install.gpuPicker.manualSubtitle')"
|
||||
:value="'unsupported'"
|
||||
:selected="selected === 'unsupported'"
|
||||
@click="pickGpu('unsupported')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- GPU Selection buttons -->
|
||||
<div
|
||||
class="flex gap-2 text-center transition-opacity"
|
||||
:class="{ selected: selected }"
|
||||
>
|
||||
<!-- NVIDIA -->
|
||||
<div
|
||||
v-if="platform !== 'darwin'"
|
||||
class="gpu-button"
|
||||
:class="{ selected: selected === 'nvidia' }"
|
||||
role="button"
|
||||
@click="pickGpu('nvidia')"
|
||||
>
|
||||
<img
|
||||
class="m-12"
|
||||
alt="NVIDIA logo"
|
||||
width="196"
|
||||
height="32"
|
||||
src="/assets/images/nvidia-logo.svg"
|
||||
/>
|
||||
</div>
|
||||
<!-- MPS -->
|
||||
<div
|
||||
v-if="platform === 'darwin'"
|
||||
class="gpu-button"
|
||||
:class="{ selected: selected === 'mps' }"
|
||||
role="button"
|
||||
@click="pickGpu('mps')"
|
||||
>
|
||||
<img
|
||||
class="rounded-lg hover-brighten"
|
||||
alt="Apple Metal Performance Shaders Logo"
|
||||
width="292"
|
||||
ratio
|
||||
src="/assets/images/apple-mps-logo.png"
|
||||
/>
|
||||
</div>
|
||||
<!-- Manual configuration -->
|
||||
<div
|
||||
class="gpu-button"
|
||||
:class="{ selected: selected === 'unsupported' }"
|
||||
role="button"
|
||||
@click="pickGpu('unsupported')"
|
||||
>
|
||||
<img
|
||||
class="m-12"
|
||||
alt="Manual configuration"
|
||||
width="196"
|
||||
src="/assets/images/manual-configuration.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details on selected GPU -->
|
||||
<p v-if="selected === 'nvidia'" class="m-1">
|
||||
<Tag icon="pi pi-check" severity="success" :value="'CUDA'" />
|
||||
{{ $t('install.gpuSelection.nvidiaDescription') }}
|
||||
</p>
|
||||
|
||||
<p v-if="selected === 'mps'" class="m-1">
|
||||
<Tag icon="pi pi-check" severity="success" :value="'MPS'" />
|
||||
{{ $t('install.gpuSelection.mpsDescription') }}
|
||||
</p>
|
||||
|
||||
<div v-if="selected === 'unsupported'" class="text-neutral-300">
|
||||
<p class="m-1">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
{{ $t('install.gpuSelection.customSkipsPython') }}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>
|
||||
{{ $t('install.gpuSelection.customComfyNeedsPython') }}
|
||||
</strong>
|
||||
</li>
|
||||
<li>{{ $t('install.gpuSelection.customManualVenv') }}</li>
|
||||
<li>{{ $t('install.gpuSelection.customInstallRequirements') }}</li>
|
||||
<li>{{ $t('install.gpuSelection.customMayNotWork') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="selected === 'cpu'">
|
||||
<p class="m-1">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
{{ $t('install.gpuSelection.cpuModeDescription') }}
|
||||
</p>
|
||||
<p class="m-1">
|
||||
{{ $t('install.gpuSelection.cpuModeDescription2') }}
|
||||
</p>
|
||||
<div class="pt-12 px-24 h-16">
|
||||
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
|
||||
<Tag
|
||||
:value="$t('install.gpuPicker.recommended')"
|
||||
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
|
||||
/>
|
||||
<i-lucide:badge-check class="text-neutral-300 text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="transition-opacity flex gap-3 h-0"
|
||||
:class="{
|
||||
'opacity-40': selected && selected !== 'cpu'
|
||||
}"
|
||||
>
|
||||
<ToggleSwitch
|
||||
v-model="cpuMode"
|
||||
input-id="cpu-mode"
|
||||
class="-translate-y-40"
|
||||
/>
|
||||
<label for="cpu-mode" class="select-none">
|
||||
{{ $t('install.gpuSelection.enableCpuMode') }}
|
||||
</label>
|
||||
<div class="text-neutral-300 px-24">
|
||||
<p v-show="descriptionText" class="leading-relaxed">
|
||||
{{ descriptionText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -132,20 +68,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import Tag from 'primevue/tag'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import HardwareOption from '@/components/install/HardwareOption.vue'
|
||||
import { st } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const cpuMode = computed({
|
||||
get: () => selected.value === 'cpu',
|
||||
set: (value) => {
|
||||
selected.value = value ? 'cpu' : null
|
||||
}
|
||||
})
|
||||
const selected = defineModel<TorchDeviceType | null>('device', {
|
||||
required: true
|
||||
})
|
||||
@@ -153,55 +81,23 @@ const selected = defineModel<TorchDeviceType | null>('device', {
|
||||
const electron = electronAPI()
|
||||
const platform = electron.getPlatform()
|
||||
|
||||
const pickGpu = (value: typeof selected.value) => {
|
||||
const newValue = selected.value === value ? null : value
|
||||
selected.value = newValue
|
||||
const showRecommendedBadge = computed(
|
||||
() => selected.value === 'mps' || selected.value === 'nvidia'
|
||||
)
|
||||
|
||||
const descriptionKeys = {
|
||||
mps: 'appleMetal',
|
||||
nvidia: 'nvidia',
|
||||
cpu: 'cpu',
|
||||
unsupported: 'manual'
|
||||
} as const
|
||||
|
||||
const descriptionText = computed(() => {
|
||||
const key = selected.value ? descriptionKeys[selected.value] : undefined
|
||||
return st(`install.gpuPicker.${key}Description`, '')
|
||||
})
|
||||
|
||||
const pickGpu = (value: TorchDeviceType) => {
|
||||
selected.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.p-tag {
|
||||
--p-tag-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hover-brighten {
|
||||
@apply transition-colors;
|
||||
transition-property: filter, box-shadow;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(107%) contrast(105%);
|
||||
box-shadow: 0 0 0.25rem #ffffff79;
|
||||
}
|
||||
}
|
||||
.p-accordioncontent-content {
|
||||
@apply bg-neutral-900 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
div.selected {
|
||||
.gpu-button:not(.selected) {
|
||||
@apply opacity-50 hover:opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
.gpu-button {
|
||||
@apply w-1/2 m-0 cursor-pointer rounded-lg flex flex-col items-center justify-around bg-neutral-800/50 hover:bg-neutral-800/75 transition-colors;
|
||||
|
||||
&.selected {
|
||||
@apply opacity-100 bg-neutral-700/50 hover:bg-neutral-700/60;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
@apply pointer-events-none opacity-40;
|
||||
}
|
||||
|
||||
.p-card-header {
|
||||
@apply text-center grow;
|
||||
}
|
||||
|
||||
.p-card-body {
|
||||
@apply text-center pt-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
73
src/components/install/HardwareOption.stories.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
|
||||
import HardwareOption from './HardwareOption.vue'
|
||||
|
||||
const meta: Meta<typeof HardwareOption> = {
|
||||
title: 'Desktop/Components/HardwareOption',
|
||||
component: HardwareOption,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [{ name: 'dark', value: '#1a1a1a' }]
|
||||
}
|
||||
},
|
||||
argTypes: {
|
||||
selected: { control: 'boolean' },
|
||||
imagePath: { control: 'text' },
|
||||
placeholderText: { control: 'text' },
|
||||
subtitle: { control: 'text' }
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const AppleMetalSelected: Story = {
|
||||
args: {
|
||||
imagePath: '/assets/images/apple-mps-logo.png',
|
||||
placeholderText: 'Apple Metal',
|
||||
subtitle: 'Apple Metal',
|
||||
value: 'mps',
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
|
||||
export const AppleMetalUnselected: Story = {
|
||||
args: {
|
||||
imagePath: '/assets/images/apple-mps-logo.png',
|
||||
placeholderText: 'Apple Metal',
|
||||
subtitle: 'Apple Metal',
|
||||
value: 'mps',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
|
||||
export const CPUOption: Story = {
|
||||
args: {
|
||||
placeholderText: 'CPU',
|
||||
subtitle: 'Subtitle',
|
||||
value: 'cpu',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
|
||||
export const ManualInstall: Story = {
|
||||
args: {
|
||||
placeholderText: 'Manual Install',
|
||||
subtitle: 'Subtitle',
|
||||
value: 'unsupported',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
|
||||
export const NvidiaSelected: Story = {
|
||||
args: {
|
||||
imagePath: '/assets/images/nvidia-logo-square.jpg',
|
||||
placeholderText: 'NVIDIA',
|
||||
subtitle: 'NVIDIA',
|
||||
value: 'nvidia',
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
55
src/components/install/HardwareOption.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<!-- Recommended Badge -->
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
'hardware-option w-[170px] h-[190px] p-5 flex flex-col items-center rounded-3xl transition-all duration-200 bg-neutral-900/70 border-4',
|
||||
selected ? 'border-solid border-brand-yellow' : 'border-transparent'
|
||||
)
|
||||
"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<!-- Icon/Logo Area - Rounded square container -->
|
||||
<div
|
||||
class="icon-container w-[110px] h-[110px] shrink-0 rounded-2xl bg-neutral-800 flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<img
|
||||
v-if="imagePath"
|
||||
:src="imagePath"
|
||||
:alt="placeholderText"
|
||||
class="w-full h-full object-cover"
|
||||
style="object-position: 57% center"
|
||||
draggable="false"
|
||||
/>
|
||||
<span v-else class="text-xl font-medium text-neutral-400">
|
||||
{{ placeholderText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Text Content -->
|
||||
<div v-if="subtitle" class="text-center mt-4">
|
||||
<div class="text-sm text-neutral-500">{{ subtitle }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface Props {
|
||||
imagePath?: string
|
||||
placeholderText: string
|
||||
subtitle?: string
|
||||
value: TorchDeviceType
|
||||
selected?: boolean
|
||||
recommended?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
defineEmits<{ click: [] }>()
|
||||
</script>
|
||||
79
src/components/install/InstallFooter.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
||||
<!-- Back button -->
|
||||
<Button
|
||||
v-if="currentStep !== '1'"
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
class="font-inter rounded-lg border-0 px-6 py-2 justify-self-start"
|
||||
@click="$emit('previous')"
|
||||
/>
|
||||
<div v-else></div>
|
||||
|
||||
<!-- Step indicators in center -->
|
||||
<StepList class="flex justify-center items-center gap-3 select-none">
|
||||
<Step value="1" :pt="stepPassthrough">
|
||||
{{ $t('install.gpu') }}
|
||||
</Step>
|
||||
<Step value="2" :disabled="disableLocationStep" :pt="stepPassthrough">
|
||||
{{ $t('install.installLocation') }}
|
||||
</Step>
|
||||
<Step value="3" :disabled="disableSettingsStep" :pt="stepPassthrough">
|
||||
{{ $t('install.desktopSettings') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
|
||||
<!-- Next/Install button -->
|
||||
<Button
|
||||
:label="currentStep !== '3' ? $t('g.next') : $t('g.install')"
|
||||
class="px-8 py-2 bg-brand-yellow hover:bg-brand-yellow/90 font-inter rounded-lg border-0 transition-colors justify-self-end"
|
||||
:pt="{
|
||||
label: { class: 'text-neutral-900 font-inter font-black' }
|
||||
}"
|
||||
:disabled="!canProceed"
|
||||
@click="currentStep !== '3' ? $emit('next') : $emit('install')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PassThrough } from '@primevue/core'
|
||||
import Button from 'primevue/button'
|
||||
import Step, { type StepPassThroughOptions } from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
|
||||
defineProps<{
|
||||
/** Current step index as string ('1', '2', '3', '4') */
|
||||
currentStep: string
|
||||
/** Whether the user can proceed to the next step */
|
||||
canProceed: boolean
|
||||
/** Whether the location step should be disabled */
|
||||
disableLocationStep: boolean
|
||||
/** Whether the migration step should be disabled */
|
||||
disableMigrationStep: boolean
|
||||
/** Whether the settings step should be disabled */
|
||||
disableSettingsStep: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
previous: []
|
||||
next: []
|
||||
install: []
|
||||
}>()
|
||||
|
||||
const stepPassthrough: PassThrough<StepPassThroughOptions> = {
|
||||
root: { class: 'flex-none p-0 m-0' },
|
||||
header: ({ context }) => ({
|
||||
class: [
|
||||
'h-2.5 p-0 m-0 border-0 rounded-full transition-all duration-300',
|
||||
context.active
|
||||
? 'bg-brand-yellow w-8 rounded-sm'
|
||||
: 'bg-neutral-700 w-2.5',
|
||||
context.disabled ? 'opacity-60 cursor-not-allowed' : ''
|
||||
].join(' ')
|
||||
}),
|
||||
number: { class: 'hidden' },
|
||||
title: { class: 'hidden' }
|
||||
}
|
||||
</script>
|
||||
148
src/components/install/InstallLocationPicker.stories.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import InstallLocationPicker from './InstallLocationPicker.vue'
|
||||
|
||||
const meta: Meta<typeof InstallLocationPicker> = {
|
||||
title: 'Desktop/Components/InstallLocationPicker',
|
||||
component: InstallLocationPicker,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
() => {
|
||||
// Mock electron API
|
||||
;(window as any).electronAPI = {
|
||||
getSystemPaths: () =>
|
||||
Promise.resolve({
|
||||
defaultInstallPath: '/Users/username/ComfyUI'
|
||||
}),
|
||||
validateInstallPath: () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false
|
||||
}),
|
||||
validateComfyUISource: () =>
|
||||
Promise.resolve({
|
||||
isValid: true
|
||||
}),
|
||||
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||
}
|
||||
return { template: '<story />' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default story with accordion expanded
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
components: { InstallLocationPicker },
|
||||
setup() {
|
||||
const installPath = ref('/Users/username/ComfyUI')
|
||||
const pathError = ref('')
|
||||
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||
|
||||
return {
|
||||
args,
|
||||
installPath,
|
||||
pathError,
|
||||
migrationSourcePath,
|
||||
migrationItemIds
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="min-h-screen bg-neutral-950 p-8">
|
||||
<InstallLocationPicker
|
||||
v-model:installPath="installPath"
|
||||
v-model:pathError="pathError"
|
||||
v-model:migrationSourcePath="migrationSourcePath"
|
||||
v-model:migrationItemIds="migrationItemIds"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
// Story with different background to test transparency
|
||||
export const OnNeutral900: Story = {
|
||||
render: (args) => ({
|
||||
components: { InstallLocationPicker },
|
||||
setup() {
|
||||
const installPath = ref('/Users/username/ComfyUI')
|
||||
const pathError = ref('')
|
||||
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||
|
||||
return {
|
||||
args,
|
||||
installPath,
|
||||
pathError,
|
||||
migrationSourcePath,
|
||||
migrationItemIds
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="min-h-screen bg-neutral-900 p-8">
|
||||
<InstallLocationPicker
|
||||
v-model:installPath="installPath"
|
||||
v-model:pathError="pathError"
|
||||
v-model:migrationSourcePath="migrationSourcePath"
|
||||
v-model:migrationItemIds="migrationItemIds"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
// Story with debug overlay showing background colors
|
||||
export const DebugBackgrounds: Story = {
|
||||
render: (args) => ({
|
||||
components: { InstallLocationPicker },
|
||||
setup() {
|
||||
const installPath = ref('/Users/username/ComfyUI')
|
||||
const pathError = ref('')
|
||||
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||
|
||||
return {
|
||||
args,
|
||||
installPath,
|
||||
pathError,
|
||||
migrationSourcePath,
|
||||
migrationItemIds
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="min-h-screen bg-neutral-950 p-8 relative">
|
||||
<div class="absolute top-4 right-4 text-white text-xs space-y-2 z-50">
|
||||
<div>Parent bg: neutral-950 (#0a0a0a)</div>
|
||||
<div>Accordion content: bg-transparent</div>
|
||||
<div>Migration options: bg-transparent + p-4 rounded-lg</div>
|
||||
</div>
|
||||
<InstallLocationPicker
|
||||
v-model:installPath="installPath"
|
||||
v-model:pathError="pathError"
|
||||
v-model:migrationSourcePath="migrationSourcePath"
|
||||
v-model:migrationItemIds="migrationItemIds"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -1,103 +1,215 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 w-[600px]">
|
||||
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
|
||||
<!-- Installation Path Section -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||
{{ $t('install.chooseInstallationLocation') }}
|
||||
<div class="grow flex flex-col gap-6 text-neutral-300">
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
{{ $t('install.locationPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<p class="text-neutral-400 my-0">
|
||||
{{ $t('install.installLocationDescription') }}
|
||||
<p class="text-center text-neutral-400 px-12">
|
||||
{{ $t('install.locationPicker.subtitle') }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<IconField class="flex-1">
|
||||
<InputText
|
||||
v-model="installPath"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': pathError }"
|
||||
@update:model-value="validatePath"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
<InputIcon
|
||||
v-tooltip.top="$t('install.installLocationTooltip')"
|
||||
class="pi pi-info-circle"
|
||||
/>
|
||||
</IconField>
|
||||
<Button icon="pi pi-folder" class="w-12" @click="browsePath" />
|
||||
<!-- Path Input -->
|
||||
<div class="flex gap-2 px-12">
|
||||
<InputText
|
||||
v-model="installPath"
|
||||
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
||||
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
|
||||
:class="{ 'p-invalid': pathError }"
|
||||
@update:model-value="validatePath"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-folder-open"
|
||||
severity="secondary"
|
||||
class="bg-neutral-700 hover:bg-neutral-600 border-0"
|
||||
@click="browsePath"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Message v-if="pathError" severity="error" class="whitespace-pre-line">
|
||||
{{ pathError }}
|
||||
</Message>
|
||||
<Message v-if="pathExists" severity="warn">
|
||||
{{ $t('install.pathExists') }}
|
||||
</Message>
|
||||
<Message v-if="nonDefaultDrive" severity="warn">
|
||||
{{ $t('install.nonDefaultDrive') }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- System Paths Info -->
|
||||
<div class="bg-neutral-800 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-medium mt-0 mb-3 text-neutral-100">
|
||||
{{ $t('install.systemLocations') }}
|
||||
</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-folder text-neutral-400" />
|
||||
<span class="text-neutral-400">App Data:</span>
|
||||
<span class="text-neutral-200">{{ appData }}</span>
|
||||
<span
|
||||
v-tooltip="$t('install.appDataLocationTooltip')"
|
||||
class="pi pi-info-circle"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-desktop text-neutral-400" />
|
||||
<span class="text-neutral-400">App Path:</span>
|
||||
<span class="text-neutral-200">{{ appPath }}</span>
|
||||
<span
|
||||
v-tooltip="$t('install.appPathLocationTooltip')"
|
||||
class="pi pi-info-circle"
|
||||
/>
|
||||
</div>
|
||||
<!-- Error Messages -->
|
||||
<div v-if="pathError || pathExists || nonDefaultDrive" class="px-12">
|
||||
<Message
|
||||
v-if="pathError"
|
||||
severity="error"
|
||||
class="whitespace-pre-line w-full"
|
||||
>
|
||||
{{ pathError }}
|
||||
</Message>
|
||||
<Message v-if="pathExists" severity="warn" class="w-full">
|
||||
{{ $t('install.pathExists') }}
|
||||
</Message>
|
||||
<Message v-if="nonDefaultDrive" severity="warn" class="w-full">
|
||||
{{ $t('install.nonDefaultDrive') }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Sections using PrimeVue Accordion -->
|
||||
<Accordion
|
||||
v-model:value="activeAccordionIndex"
|
||||
:multiple="true"
|
||||
class="location-picker-accordion"
|
||||
:pt="{
|
||||
root: 'bg-transparent border-0',
|
||||
panel: {
|
||||
root: 'border-0 mb-0'
|
||||
},
|
||||
header: {
|
||||
root: 'border-0',
|
||||
content:
|
||||
'text-neutral-400 hover:text-neutral-300 px-4 py-2 flex items-center gap-3',
|
||||
toggleicon: 'text-xs order-first mr-0'
|
||||
},
|
||||
content: {
|
||||
root: 'bg-transparent border-0',
|
||||
content: 'text-neutral-500 text-sm pl-11 pb-3 pt-0'
|
||||
}
|
||||
}"
|
||||
>
|
||||
<AccordionPanel value="0">
|
||||
<AccordionHeader>
|
||||
{{ $t('install.locationPicker.migrateFromExisting') }}
|
||||
</AccordionHeader>
|
||||
<AccordionContent>
|
||||
<MigrationPicker
|
||||
v-model:source-path="migrationSourcePath"
|
||||
v-model:migration-item-ids="migrationItemIds"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
|
||||
<AccordionPanel value="1">
|
||||
<AccordionHeader>
|
||||
{{ $t('install.locationPicker.chooseDownloadServers') }}
|
||||
</AccordionHeader>
|
||||
<AccordionContent>
|
||||
<template
|
||||
v-for="([item, modelValue], index) in mirrors"
|
||||
:key="item.settingId + item.mirror"
|
||||
>
|
||||
<Divider v-if="index > 0" class="my-8" />
|
||||
|
||||
<MirrorItem
|
||||
v-model="modelValue.value"
|
||||
:item="item"
|
||||
@state-change="validationStates[index] = $event"
|
||||
/>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
|
||||
import Accordion from 'primevue/accordion'
|
||||
import AccordionContent from 'primevue/accordioncontent'
|
||||
import AccordionHeader from 'primevue/accordionheader'
|
||||
import AccordionPanel from 'primevue/accordionpanel'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import Divider from 'primevue/divider'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { type ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import {
|
||||
PYPI_MIRROR,
|
||||
PYTHON_MIRROR,
|
||||
type UVMirror
|
||||
} from '@/constants/uvMirrors'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const installPath = defineModel<string>('installPath', { required: true })
|
||||
const pathError = defineModel<string>('pathError', { required: true })
|
||||
const migrationSourcePath = defineModel<string>('migrationSourcePath')
|
||||
const migrationItemIds = defineModel<string[]>('migrationItemIds')
|
||||
const pythonMirror = defineModel<string>('pythonMirror', {
|
||||
default: ''
|
||||
})
|
||||
const pypiMirror = defineModel<string>('pypiMirror', {
|
||||
default: ''
|
||||
})
|
||||
const torchMirror = defineModel<string>('torchMirror', {
|
||||
default: ''
|
||||
})
|
||||
|
||||
const { device } = defineProps<{ device: TorchDeviceType | null }>()
|
||||
|
||||
const pathExists = ref(false)
|
||||
const nonDefaultDrive = ref(false)
|
||||
const appData = ref('')
|
||||
const appPath = ref('')
|
||||
const inputTouched = ref(false)
|
||||
|
||||
// Accordion state - array of active panel values
|
||||
const activeAccordionIndex = ref<string[] | undefined>(undefined)
|
||||
|
||||
const electron = electronAPI()
|
||||
|
||||
// Get system paths on component mount
|
||||
// Mirror configuration logic
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.NightlyCpu,
|
||||
fallbackMirror: TorchMirrorUrl.NightlyCpu
|
||||
}
|
||||
case 'nvidia':
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.Cuda,
|
||||
fallbackMirror: TorchMirrorUrl.Cuda
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
return {
|
||||
settingId,
|
||||
mirror: PYPI_MIRROR.mirror,
|
||||
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userIsInChina = ref(false)
|
||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||
...mirror,
|
||||
mirror: mirror.fallbackMirror
|
||||
})
|
||||
|
||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
|
||||
(
|
||||
[
|
||||
[PYTHON_MIRROR, pythonMirror],
|
||||
[PYPI_MIRROR, pypiMirror],
|
||||
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
|
||||
] as [UVMirror, ModelRef<string>][]
|
||||
).map(([item, modelValue]) => [
|
||||
userIsInChina.value ? useFallbackMirror(item) : item,
|
||||
modelValue
|
||||
])
|
||||
)
|
||||
|
||||
const validationStates = ref<ValidationState[]>(
|
||||
mirrors.value.map(() => ValidationState.IDLE)
|
||||
)
|
||||
|
||||
// Get default install path on component mount
|
||||
onMounted(async () => {
|
||||
const paths = await electron.getSystemPaths()
|
||||
appData.value = paths.appData
|
||||
appPath.value = paths.appPath
|
||||
installPath.value = paths.defaultInstallPath
|
||||
|
||||
await validatePath(paths.defaultInstallPath)
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
|
||||
const validatePath = async (path: string | undefined) => {
|
||||
@@ -151,3 +263,52 @@ const onFocus = async () => {
|
||||
await validatePath(installPath.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.location-picker-accordion) {
|
||||
@apply px-12;
|
||||
|
||||
.p-accordionpanel {
|
||||
@apply border-0 bg-transparent;
|
||||
}
|
||||
|
||||
.p-accordionheader {
|
||||
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-radius 0.5s ease;
|
||||
}
|
||||
|
||||
/* When panel is expanded, adjust header border radius */
|
||||
.p-accordionpanel-active {
|
||||
.p-accordionheader {
|
||||
@apply rounded-t-xl rounded-b-none;
|
||||
}
|
||||
}
|
||||
|
||||
.p-accordioncontent {
|
||||
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
|
||||
}
|
||||
|
||||
.p-accordioncontent-content {
|
||||
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
|
||||
}
|
||||
|
||||
/* Override default chevron icons to use up/down */
|
||||
.p-accordionheader-toggle-icon {
|
||||
&::before {
|
||||
content: '\e933';
|
||||
}
|
||||
}
|
||||
|
||||
.p-accordionpanel-active {
|
||||
.p-accordionheader-toggle-icon {
|
||||
&::before {
|
||||
content: '\e902';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
45
src/components/install/MigrationPicker.stories.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MigrationPicker from './MigrationPicker.vue'
|
||||
|
||||
const meta: Meta<typeof MigrationPicker> = {
|
||||
title: 'Desktop/Components/MigrationPicker',
|
||||
component: MigrationPicker,
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' }
|
||||
]
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
() => {
|
||||
;(window as any).electronAPI = {
|
||||
validateComfyUISource: () => Promise.resolve({ isValid: true }),
|
||||
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||
}
|
||||
|
||||
return { template: '<story />' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { MigrationPicker },
|
||||
setup() {
|
||||
const sourcePath = ref('')
|
||||
const migrationItemIds = ref<string[]>([])
|
||||
return { sourcePath, migrationItemIds }
|
||||
},
|
||||
template:
|
||||
'<MigrationPicker v-model:sourcePath="sourcePath" v-model:migrationItemIds="migrationItemIds" />'
|
||||
})
|
||||
}
|
||||
@@ -2,10 +2,6 @@
|
||||
<div class="flex flex-col gap-6 w-[600px]">
|
||||
<!-- Source Location Section -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||
{{ $t('install.migrateFromExistingInstallation') }}
|
||||
</h2>
|
||||
|
||||
<p class="text-neutral-400 my-0">
|
||||
{{ $t('install.migrationSourcePathDescription') }}
|
||||
</p>
|
||||
@@ -13,7 +9,7 @@
|
||||
<div class="flex gap-2">
|
||||
<InputText
|
||||
v-model="sourcePath"
|
||||
placeholder="Select existing ComfyUI installation (optional)"
|
||||
:placeholder="$t('install.locationPicker.migrationPathPlaceholder')"
|
||||
class="flex-1"
|
||||
:class="{ 'p-invalid': pathError }"
|
||||
@update:model-value="validateSource"
|
||||
@@ -27,10 +23,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Migration Options -->
|
||||
<div
|
||||
v-if="isValidSource"
|
||||
class="flex flex-col gap-4 bg-neutral-800 p-4 rounded-lg"
|
||||
>
|
||||
<div v-if="isValidSource" class="flex flex-col gap-4 p-4 rounded-lg">
|
||||
<h3 class="text-lg mt-0 font-medium text-neutral-100">
|
||||
{{ $t('install.selectItemsToMigrate') }}
|
||||
</h3>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<Panel
|
||||
:header="$t('install.settings.mirrorSettings')"
|
||||
toggleable
|
||||
:collapsed="!showMirrorInputs"
|
||||
pt:root="bg-neutral-800 border-none w-[600px]"
|
||||
>
|
||||
<template
|
||||
v-for="([item, modelValue], index) in mirrors"
|
||||
:key="item.settingId + item.mirror"
|
||||
>
|
||||
<Divider v-if="index > 0" />
|
||||
|
||||
<MirrorItem
|
||||
v-model="modelValue.value"
|
||||
:item="item"
|
||||
@state-change="validationStates[index] = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #icons>
|
||||
<i
|
||||
v-tooltip="validationStateTooltip"
|
||||
:class="{
|
||||
'pi pi-spin pi-spinner text-neutral-400':
|
||||
validationState === ValidationState.LOADING,
|
||||
'pi pi-check text-green-500':
|
||||
validationState === ValidationState.VALID,
|
||||
'pi pi-times text-red-500':
|
||||
validationState === ValidationState.INVALID
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
TorchDeviceType,
|
||||
TorchMirrorUrl
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Divider from 'primevue/divider'
|
||||
import Panel from 'primevue/panel'
|
||||
import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
const showMirrorInputs = ref(false)
|
||||
const { device } = defineProps<{ device: TorchDeviceType | null }>()
|
||||
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
||||
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.NightlyCpu,
|
||||
fallbackMirror: TorchMirrorUrl.NightlyCpu
|
||||
}
|
||||
case 'nvidia':
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.Cuda,
|
||||
fallbackMirror: TorchMirrorUrl.Cuda
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
return {
|
||||
settingId,
|
||||
mirror: PYPI_MIRROR.mirror,
|
||||
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userIsInChina = ref(false)
|
||||
onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
|
||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||
...mirror,
|
||||
mirror: mirror.fallbackMirror
|
||||
})
|
||||
|
||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
|
||||
(
|
||||
[
|
||||
[PYTHON_MIRROR, pythonMirror],
|
||||
[PYPI_MIRROR, pypiMirror],
|
||||
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
|
||||
] as [UVMirror, ModelRef<string>][]
|
||||
).map(([item, modelValue]) => [
|
||||
userIsInChina.value ? useFallbackMirror(item) : item,
|
||||
modelValue
|
||||
])
|
||||
)
|
||||
|
||||
const validationStates = ref<ValidationState[]>(
|
||||
mirrors.value.map(() => ValidationState.IDLE)
|
||||
)
|
||||
const validationState = computed(() => {
|
||||
return mergeValidationStates(validationStates.value)
|
||||
})
|
||||
const validationStateTooltip = computed(() => {
|
||||
switch (validationState.value) {
|
||||
case ValidationState.INVALID:
|
||||
return t('install.settings.mirrorsUnreachable')
|
||||
case ValidationState.VALID:
|
||||
return t('install.settings.mirrorsReachable')
|
||||
default:
|
||||
return t('install.settings.checkingMirrors')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg font-medium text-neutral-100">
|
||||
<div class="flex flex-col gap-4 text-neutral-400 text-sm">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-neutral-100 mb-3 mt-0">
|
||||
{{ $t(`settings.${normalizedSettingId}.name`) }}
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-400 mt-1">
|
||||
<p class="my-1">
|
||||
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -16,18 +16,61 @@
|
||||
"
|
||||
@state-change="validationState = $event"
|
||||
/>
|
||||
<div v-if="secondParagraph" class="mt-2">
|
||||
<a href="#" @click.prevent="showDialog = true">
|
||||
{{ $t('g.learnMore') }}
|
||||
</a>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="showDialog"
|
||||
modal
|
||||
dismissable-mask
|
||||
:header="$t(`settings.${normalizedSettingId}.urlFormatTitle`)"
|
||||
class="select-none max-w-3xl"
|
||||
>
|
||||
<div class="text-neutral-300">
|
||||
<p class="mt-1 whitespace-pre-wrap">{{ secondParagraph }}</p>
|
||||
<div class="mt-2 break-all">
|
||||
<span class="text-neutral-300 font-semibold">
|
||||
{{ EXAMPLE_URL_FIRST_PART }}
|
||||
</span>
|
||||
<span>{{ EXAMPLE_URL_SECOND_PART }}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<p>
|
||||
{{ $t(`settings.${normalizedSettingId}.fileUrlDescription`) }}
|
||||
</p>
|
||||
<span class="text-neutral-300 font-semibold">
|
||||
{{ FILE_URL_SCHEME }}
|
||||
</span>
|
||||
<span>
|
||||
{{ EXAMPLE_FILE_URL }}
|
||||
</span>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Divider from 'primevue/divider'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { UVMirror } from '@/constants/uvMirrors'
|
||||
import { st } from '@/i18n'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const FILE_URL_SCHEME = 'file://'
|
||||
const EXAMPLE_FILE_URL = '/C:/MyPythonInstallers/'
|
||||
const EXAMPLE_URL_FIRST_PART =
|
||||
'https://github.com/astral-sh/python-build-standalone/releases/download'
|
||||
const EXAMPLE_URL_SECOND_PART =
|
||||
'/20250902/cpython-3.12.11+20250902-x86_64-pc-windows-msvc-install_only.tar.gz'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: UVMirror
|
||||
}>()
|
||||
@@ -38,11 +81,16 @@ const emit = defineEmits<{
|
||||
|
||||
const modelValue = defineModel<string>('modelValue', { required: true })
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
const showDialog = ref(false)
|
||||
|
||||
const normalizedSettingId = computed(() => {
|
||||
return normalizeI18nKey(item.settingId)
|
||||
})
|
||||
|
||||
const secondParagraph = computed(() =>
|
||||
st(`settings.${normalizedSettingId.value}.urlDescription`, '')
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
modelValue.value = item.mirror
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="workflowTabRef"
|
||||
class="flex p-2 gap-2 workflow-tab"
|
||||
class="flex p-2 gap-2 workflow-tab group"
|
||||
v-bind="$attrs"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@@ -11,9 +11,13 @@
|
||||
{{ workflowOption.workflow.filename }}
|
||||
</span>
|
||||
<div class="relative">
|
||||
<span v-if="shouldShowStatusIndicator" class="status-indicator">•</span>
|
||||
<span
|
||||
v-if="shouldShowStatusIndicator"
|
||||
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4"
|
||||
>•</span
|
||||
>
|
||||
<Button
|
||||
class="close-button p-0 w-auto"
|
||||
class="close-button p-0 w-auto invisible"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -174,18 +178,6 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.status-indicator {
|
||||
@apply absolute font-bold;
|
||||
font-size: 1.5rem;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.p-tooltip.workflow-tab-tooltip {
|
||||
z-index: 1200 !important;
|
||||
|
||||
@@ -360,14 +360,6 @@ onUpdated(() => {
|
||||
@apply visible;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:hover) .status-indicator {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) .close-button {
|
||||
@apply invisible;
|
||||
}
|
||||
|
||||
:deep(.p-scrollpanel-content) {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
const fitAddon = new FitAddon()
|
||||
const terminal = markRaw(
|
||||
new Terminal({
|
||||
convertEol: true
|
||||
convertEol: true,
|
||||
theme: { background: '#171717' }
|
||||
})
|
||||
)
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
|
||||
}
|
||||
|
||||
// ---- constants ----
|
||||
const SORA_SIZES = {
|
||||
BASIC: new Set(['720x1280', '1280x720']),
|
||||
PRO: new Set(['1024x1792', '1792x1024'])
|
||||
}
|
||||
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
|
||||
|
||||
// ---- sora-2 pricing helpers ----
|
||||
function validateSora2Selection(
|
||||
modelRaw: string,
|
||||
duration: number,
|
||||
sizeRaw: string
|
||||
): string | undefined {
|
||||
const model = modelRaw?.toLowerCase() ?? ''
|
||||
const size = sizeRaw?.toLowerCase() ?? ''
|
||||
|
||||
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
|
||||
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
|
||||
if (!ALL_SIZES.has(size))
|
||||
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
|
||||
|
||||
if (model.includes('sora-2-pro')) return undefined
|
||||
|
||||
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
|
||||
return 'sora-2 supports only 720x1280 or 1280x720'
|
||||
|
||||
if (!model.includes('sora-2')) return 'Unsupported model'
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
|
||||
const model = modelRaw?.toLowerCase() ?? ''
|
||||
const size = sizeRaw?.toLowerCase() ?? ''
|
||||
|
||||
if (model.includes('sora-2-pro')) {
|
||||
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
|
||||
}
|
||||
if (model.includes('sora-2')) return 0.1
|
||||
|
||||
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
|
||||
}
|
||||
|
||||
function formatRunPrice(perSec: number, duration: number) {
|
||||
return `$${(perSec * duration).toFixed(2)}/Run`
|
||||
}
|
||||
|
||||
// ---- pricing calculator ----
|
||||
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
|
||||
const getWidgetValue = (name: string) =>
|
||||
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
|
||||
|
||||
const model = getWidgetValue('model')
|
||||
const size = getWidgetValue('size')
|
||||
const duration = Number(
|
||||
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
|
||||
?.value
|
||||
)
|
||||
|
||||
if (!model || !size || !duration) return 'Set model, duration & size'
|
||||
|
||||
const validationError = validateSora2Selection(model, duration, size)
|
||||
if (validationError) return validationError
|
||||
|
||||
const perSec = perSecForSora2(model, size)
|
||||
return formatRunPrice(perSec, duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Static pricing data for API nodes, now supporting both strings and functions
|
||||
*/
|
||||
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
FluxProKontextMaxNode: {
|
||||
displayPrice: '$0.08/Run'
|
||||
},
|
||||
OpenAIVideoSora2: {
|
||||
displayPrice: sora2PricingCalculator
|
||||
},
|
||||
IdeogramV1: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const numImagesWidget = node.widgets?.find(
|
||||
@@ -1548,6 +1619,74 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
},
|
||||
ByteDanceImageReferenceNode: {
|
||||
displayPrice: byteDanceVideoPricingCalculator
|
||||
},
|
||||
WanTextToVideoApi: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'size'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
|
||||
|
||||
const seconds = parseFloat(String(durationWidget.value))
|
||||
const resolutionStr = String(resolutionWidget.value).toLowerCase()
|
||||
|
||||
const resKey = resolutionStr.includes('1080')
|
||||
? '1080p'
|
||||
: resolutionStr.includes('720')
|
||||
? '720p'
|
||||
: resolutionStr.includes('480')
|
||||
? '480p'
|
||||
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
|
||||
|
||||
const pricePerSecond: Record<string, number> = {
|
||||
'480p': 0.05,
|
||||
'720p': 0.1,
|
||||
'1080p': 0.15
|
||||
}
|
||||
|
||||
const pps = pricePerSecond[resKey]
|
||||
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
|
||||
|
||||
const cost = (pps * seconds).toFixed(2)
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
},
|
||||
WanImageToVideoApi: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'resolution'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
|
||||
|
||||
const seconds = parseFloat(String(durationWidget.value))
|
||||
const resolution = String(resolutionWidget.value).trim().toLowerCase()
|
||||
|
||||
const pricePerSecond: Record<string, number> = {
|
||||
'480p': 0.05,
|
||||
'720p': 0.1,
|
||||
'1080p': 0.15
|
||||
}
|
||||
|
||||
const pps = pricePerSecond[resolution]
|
||||
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
|
||||
|
||||
const cost = (pps * seconds).toFixed(2)
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
},
|
||||
WanTextToImageApi: {
|
||||
displayPrice: '$0.03/Run'
|
||||
},
|
||||
WanImageToImageApi: {
|
||||
displayPrice: '$0.03/Run'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1589,6 +1728,7 @@ export const useNodePricing = () => {
|
||||
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
||||
OpenAIDalle3: ['size', 'quality'],
|
||||
OpenAIDalle2: ['size', 'n'],
|
||||
OpenAIVideoSora2: ['model', 'size', 'duration'],
|
||||
OpenAIGPTImage1: ['quality', 'n'],
|
||||
IdeogramV1: ['num_images', 'turbo'],
|
||||
IdeogramV2: ['num_images', 'turbo'],
|
||||
@@ -1647,7 +1787,9 @@ export const useNodePricing = () => {
|
||||
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution']
|
||||
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
||||
WanTextToVideoApi: ['duration', 'size'],
|
||||
WanImageToVideoApi: ['duration', 'resolution']
|
||||
}
|
||||
return widgetMap[nodeType] || []
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@ export function useManagerState() {
|
||||
)
|
||||
|
||||
// Check command line args first (highest priority)
|
||||
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
|
||||
// --enable-manager flag enables the manager (opposite of old --disable-manager)
|
||||
const hasEnableManager =
|
||||
systemStats.value?.system?.argv?.includes('--enable-manager')
|
||||
|
||||
// If --enable-manager is NOT present, manager is disabled
|
||||
if (!hasEnableManager) {
|
||||
return ManagerUIState.DISABLED
|
||||
}
|
||||
|
||||
|
||||
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]) }
|
||||
}
|
||||
@@ -4,11 +4,14 @@ import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
|
||||
import type { ISubgraphInput } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
type INodeInputSlot,
|
||||
type ISlotType,
|
||||
type NodeId
|
||||
import type {
|
||||
ISubgraphInput,
|
||||
IWidgetLocator
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
ISlotType,
|
||||
NodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
|
||||
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
|
||||
@@ -78,9 +81,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const existingInput = this.inputs.find((i) => i.name == name)
|
||||
if (existingInput) {
|
||||
const linkId = subgraphInput.linkIds[0]
|
||||
const { inputNode } = subgraph.links[linkId].resolve(subgraph)
|
||||
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
|
||||
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
|
||||
if (widget) this.#setWidget(subgraphInput, existingInput, widget)
|
||||
if (widget)
|
||||
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
|
||||
return
|
||||
}
|
||||
const input = this.addInput(name, type)
|
||||
@@ -185,13 +189,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
subgraphInput.events.addEventListener(
|
||||
'input-connected',
|
||||
() => {
|
||||
(e) => {
|
||||
if (input._widget) return
|
||||
|
||||
const widget = subgraphInput._widget
|
||||
if (!widget) return
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
const widgetLocator = e.detail.input.widget
|
||||
this.#setWidget(subgraphInput, input, widget, widgetLocator)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
@@ -301,7 +306,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const widget = resolved.inputNode.getWidgetFromSlot(resolved.input)
|
||||
if (!widget) continue
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
this.#setWidget(subgraphInput, input, widget, resolved.input.widget)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -310,11 +315,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
#setWidget(
|
||||
subgraphInput: Readonly<SubgraphInput>,
|
||||
input: INodeInputSlot,
|
||||
widget: Readonly<IBaseWidget>
|
||||
widget: Readonly<IBaseWidget>,
|
||||
inputWidget: IWidgetLocator | undefined
|
||||
) {
|
||||
// Use the first matching widget
|
||||
const targetWidget = toConcreteWidget(widget, this)
|
||||
const promotedWidget = targetWidget.createCopyForNode(this)
|
||||
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
|
||||
this
|
||||
)
|
||||
|
||||
Object.assign(promotedWidget, {
|
||||
get name() {
|
||||
@@ -372,11 +379,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// NOTE: This code creates linked chains of prototypes for passing across
|
||||
// multiple levels of subgraphs. As part of this, it intentionally avoids
|
||||
// creating new objects. Have care when making changes.
|
||||
const backingInput =
|
||||
targetWidget.node.findInputSlot(widget.name, true)?.widget ?? {}
|
||||
input.widget ??= { name: subgraphInput.name }
|
||||
input.widget.name = subgraphInput.name
|
||||
Object.setPrototypeOf(input.widget, backingInput)
|
||||
if (inputWidget) Object.setPrototypeOf(input.widget, inputWidget)
|
||||
|
||||
input._widget = promotedWidget
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "تصدير سير العمل (تنسيق API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "إرسال ملاحظات"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "تحويل التحديد إلى رسم فرعي"
|
||||
},
|
||||
@@ -170,9 +167,6 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "تحميل سير العمل الافتراضي"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "تبديل مدير العقد المخصصة"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "تبديل شريط تقدم مدير العقد المخصصة"
|
||||
},
|
||||
@@ -291,4 +285,4 @@
|
||||
"label": "تبديل الشريط الجانبي لسير العمل",
|
||||
"tooltip": "سير العمل"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,6 @@
|
||||
"frontendNewer": "إصدار الواجهة الأمامية {frontendVersion} قد لا يكون متوافقاً مع الإصدار الخلفي {backendVersion}.",
|
||||
"frontendOutdated": "إصدار الواجهة الأمامية {frontendVersion} قديم. يتطلب الإصدار الخلفي {requiredVersion} أو أحدث.",
|
||||
"goToNode": "الانتقال إلى العقدة",
|
||||
"help": "مساعدة",
|
||||
"icon": "أيقونة",
|
||||
"imageFailedToLoad": "فشل تحميل الصورة",
|
||||
"imageUrl": "رابط الصورة",
|
||||
@@ -778,7 +777,6 @@
|
||||
"File": "ملف",
|
||||
"Fit Group To Contents": "ملائمة المجموعة للمحتويات",
|
||||
"Focus Mode": "وضع التركيز",
|
||||
"Give Feedback": "تقديم ملاحظات",
|
||||
"Group Selected Nodes": "تجميع العقد المحددة",
|
||||
"Help": "مساعدة",
|
||||
"Help Center": "مركز المساعدة",
|
||||
@@ -837,7 +835,6 @@
|
||||
"Toggle Terminal Bottom Panel": "تبديل لوحة الطرفية السفلية",
|
||||
"Toggle Theme (Dark/Light)": "تبديل السمة (داكن/فاتح)",
|
||||
"Toggle View Controls Bottom Panel": "تبديل لوحة عناصر التحكم في العرض السفلية",
|
||||
"Toggle the Custom Nodes Manager": "تبديل مدير العقد المخصصة",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "تبديل شريط تقدم مدير العقد المخصصة",
|
||||
"Undo": "تراجع",
|
||||
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
|
||||
@@ -931,9 +928,6 @@
|
||||
"upscale_diffusion": "انتشار التكبير",
|
||||
"upscaling": "تكبير",
|
||||
"utils": "أدوات مساعدة",
|
||||
"v1": "الإصدار 1",
|
||||
"v2": "الإصدار 2",
|
||||
"v3": "الإصدار 3",
|
||||
"video": "فيديو",
|
||||
"video_models": "نماذج الفيديو"
|
||||
},
|
||||
@@ -1693,4 +1687,4 @@
|
||||
"showMinimap": "إظهار الخريطة المصغرة",
|
||||
"zoomToFit": "تكبير لتناسب الشاشة"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "يحفظ ملفات SVG على القرص.",
|
||||
"display_name": "حفظ SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "بادئة اسم الملف",
|
||||
"tooltip": "بادئة اسم الملف للحفظ. يمكن أن تتضمن معلومات تنسيق مثل %date:yyyy-MM-dd% أو %Empty Latent Image.width% لاستخدام قيم من العقد."
|
||||
},
|
||||
"svg": {
|
||||
"name": "ملف SVG"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "يحفظ الصور المدخلة في مجلد مخرجات ComfyUI الخاص بك.",
|
||||
"display_name": "حفظ الفيديو",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "شريط الأعلى (الصف الثاني)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "عتبة التكبير للرسم بجودة منخفضة",
|
||||
"tooltip": "عرض أشكال بجودة منخفضة عند التكبير للخارج"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "الحد الأقصى للإطارات في الثانية",
|
||||
"tooltip": "الحد الأقصى لعدد الإطارات في الثانية التي يسمح للرسم أن يعرضها. يحد من استخدام GPU على حساب السلاسة. إذا كانت 0، يتم استخدام معدل تحديث الشاشة. الافتراضي: 0"
|
||||
@@ -413,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "الالتصاق بالشبكة دائمًا"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Export Workflow (API Format)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Give Feedback"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convert Selection to Subgraph"
|
||||
},
|
||||
@@ -260,6 +257,9 @@
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Sign Out"
|
||||
},
|
||||
"Experimental_ToggleVueNodes": {
|
||||
"label": "Experimental: Enable Vue Nodes"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Close Current Workflow"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"calculatingDimensions": "Calculating dimensions",
|
||||
"import": "Import",
|
||||
"loadAllFolders": "Load All Folders",
|
||||
"logoAlt": "ComfyUI Logo",
|
||||
"refresh": "Refresh",
|
||||
"refreshNode": "Refresh Node",
|
||||
"terminal": "Terminal",
|
||||
@@ -406,6 +407,27 @@
|
||||
"migration": "Migration",
|
||||
"desktopSettings": "Desktop Settings",
|
||||
"chooseInstallationLocation": "Choose Installation Location",
|
||||
"gpuPicker": {
|
||||
"title": "Choose your hardware setup",
|
||||
"recommended": "RECOMMENDED",
|
||||
"nvidiaSubtitle": "NVIDIA CUDA",
|
||||
"cpuSubtitle": "CPU Mode",
|
||||
"manualSubtitle": "Manual Setup",
|
||||
"appleMetalDescription": "Leverages your Mac's GPU for faster speed and a better overall experience",
|
||||
"nvidiaDescription": "Use your NVIDIA GPU with CUDA acceleration for the best performance.",
|
||||
"cpuDescription": "Use CPU mode for compatibility when GPU acceleration is not available",
|
||||
"manualDescription": "Configure ComfyUI manually for advanced setups or unsupported hardware"
|
||||
},
|
||||
"locationPicker": {
|
||||
"title": "Choose where to install ComfyUI",
|
||||
"subtitle": "Pick a folder for ComfyUI's files. We'll also set up Python there automatically.",
|
||||
"pathPlaceholder": "/Users/username/Documents/ComfyUI",
|
||||
"migrationPathPlaceholder": "Select existing ComfyUI installation (optional)",
|
||||
"migrateFromExisting": "Migrate from existing installation",
|
||||
"migrateDescription": "Copy or link your existing models, custom nodes, and configurations from a previous ComfyUI installation.",
|
||||
"chooseDownloadServers": "Choose download servers manually",
|
||||
"downloadServersDescription": "Select specific mirror servers for downloading Python, PyPI packages, and PyTorch based on your location."
|
||||
},
|
||||
"systemLocations": "System Locations",
|
||||
"failedToSelectDirectory": "Failed to select directory",
|
||||
"pathValidationFailed": "Failed to validate path",
|
||||
@@ -490,18 +512,26 @@
|
||||
"metricsDisabled": "Metrics Disabled",
|
||||
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
||||
},
|
||||
"desktopStart": {
|
||||
"initialising": "Initialising..."
|
||||
},
|
||||
"serverStart": {
|
||||
"title": "Starting ComfyUI",
|
||||
"troubleshoot": "Troubleshoot",
|
||||
"reportIssue": "Report Issue",
|
||||
"openLogs": "Open Logs",
|
||||
"showTerminal": "Show Terminal",
|
||||
"copySelectionTooltip": "Copy selection",
|
||||
"copyAllTooltip": "Copy all",
|
||||
"errorMessage": "Unable to start ComfyUI Desktop",
|
||||
"installation": {
|
||||
"title": "Installing ComfyUI"
|
||||
},
|
||||
"process": {
|
||||
"initial-state": "Loading...",
|
||||
"python-setup": "Setting up Python Environment...",
|
||||
"starting-server": "Starting ComfyUI server...",
|
||||
"ready": "Finishing...",
|
||||
"ready": "Loading Human Interface",
|
||||
"error": "Unable to start ComfyUI Desktop"
|
||||
}
|
||||
},
|
||||
@@ -1073,7 +1103,7 @@
|
||||
"queue": "Queue Panel"
|
||||
},
|
||||
"menuLabels": {
|
||||
"Workflow": "Workflow",
|
||||
"File": "File",
|
||||
"Edit": "Edit",
|
||||
"View": "View",
|
||||
"Help": "Help",
|
||||
@@ -1092,7 +1122,6 @@
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Lock Canvas": "Lock Canvas",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
@@ -1101,8 +1130,9 @@
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
"Node Links": "Node Links",
|
||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||
"Minimap": "Minimap",
|
||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||
@@ -1118,7 +1148,6 @@
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
@@ -1137,10 +1166,11 @@
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
|
||||
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
|
||||
"Install Missing": "Install Missing",
|
||||
"Install Missing Custom Nodes": "Install Missing Custom Nodes",
|
||||
"Check for Custom Node Updates": "Check for Custom Node Updates",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
|
||||
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"Unload Models": "Unload Models",
|
||||
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
|
||||
@@ -1163,31 +1193,22 @@
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
"Sign Out": "Sign Out",
|
||||
"Experimental: Enable Vue Nodes": "Experimental: Enable Vue Nodes",
|
||||
"Close Current Workflow": "Close Current Workflow",
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
"Toggle Search Box": "Toggle Search Box",
|
||||
"Bottom Panel": "Bottom Panel",
|
||||
"Toggle Bottom Panel": "Toggle Bottom Panel",
|
||||
"Show Keybindings Dialog": "Show Keybindings Dialog",
|
||||
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
|
||||
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
|
||||
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
|
||||
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
|
||||
"Toggle Focus Mode": "Toggle Focus Mode",
|
||||
"Focus Mode": "Focus Mode",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Queue Panel": "Queue Panel",
|
||||
"Workflows": "Workflows",
|
||||
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
|
||||
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
|
||||
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
|
||||
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar",
|
||||
"sideToolbar_modelLibrary": "sideToolbar.modelLibrary",
|
||||
"sideToolbar_nodeLibrary": "sideToolbar.nodeLibrary",
|
||||
"sideToolbar_queue": "sideToolbar.queue",
|
||||
"sideToolbar_workflows": "sideToolbar.workflows"
|
||||
"Workflows": "Workflows"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
@@ -1248,7 +1269,9 @@
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences",
|
||||
"3DViewer": "3DViewer",
|
||||
"Vue Nodes": "Vue Nodes"
|
||||
"Vue Nodes": "Vue Nodes",
|
||||
"Assets": "Assets",
|
||||
"Canvas Navigation": "Canvas Navigation"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1385,42 +1408,49 @@
|
||||
"noise": "noise",
|
||||
"sampling": "sampling",
|
||||
"schedulers": "schedulers",
|
||||
"audio": "audio",
|
||||
"conditioning": "conditioning",
|
||||
"loaders": "loaders",
|
||||
"guiders": "guiders",
|
||||
"api node": "api node",
|
||||
"video": "video",
|
||||
"ByteDance": "ByteDance",
|
||||
"image": "image",
|
||||
"preprocessors": "preprocessors",
|
||||
"utils": "utils",
|
||||
"string": "string",
|
||||
"advanced": "advanced",
|
||||
"guidance": "guidance",
|
||||
"loaders": "loaders",
|
||||
"model_merging": "model_merging",
|
||||
"model_patches": "model_patches",
|
||||
"chroma_radiance": "chroma_radiance",
|
||||
"attention_experiments": "attention_experiments",
|
||||
"conditioning": "conditioning",
|
||||
"flux": "flux",
|
||||
"hooks": "hooks",
|
||||
"combine": "combine",
|
||||
"cond single": "cond single",
|
||||
"context": "context",
|
||||
"controlnet": "controlnet",
|
||||
"inpaint": "inpaint",
|
||||
"scheduling": "scheduling",
|
||||
"create": "create",
|
||||
"video": "video",
|
||||
"mask": "mask",
|
||||
"deprecated": "deprecated",
|
||||
"debug": "debug",
|
||||
"model": "model",
|
||||
"latent": "latent",
|
||||
"audio": "audio",
|
||||
"3d": "3d",
|
||||
"ltxv": "ltxv",
|
||||
"sd3": "sd3",
|
||||
"sigmas": "sigmas",
|
||||
"api node": "api node",
|
||||
"BFL": "BFL",
|
||||
"model_patches": "model_patches",
|
||||
"unet": "unet",
|
||||
"Gemini": "Gemini",
|
||||
"text": "text",
|
||||
"gligen": "gligen",
|
||||
"video_models": "video_models",
|
||||
"sd": "sd",
|
||||
"Ideogram": "Ideogram",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"postprocessing": "postprocessing",
|
||||
"transform": "transform",
|
||||
"batch": "batch",
|
||||
@@ -1430,34 +1460,44 @@
|
||||
"Kling": "Kling",
|
||||
"samplers": "samplers",
|
||||
"operations": "operations",
|
||||
"training": "training",
|
||||
"lotus": "lotus",
|
||||
"Luma": "Luma",
|
||||
"MiniMax": "MiniMax",
|
||||
"debug": "debug",
|
||||
"model": "model",
|
||||
"model_specific": "model_specific",
|
||||
"Moonvalley Marey": "Moonvalley Marey",
|
||||
"OpenAI": "OpenAI",
|
||||
"cond pair": "cond pair",
|
||||
"photomaker": "photomaker",
|
||||
"Pika": "Pika",
|
||||
"PixVerse": "PixVerse",
|
||||
"utils": "utils",
|
||||
"primitive": "primitive",
|
||||
"qwen": "qwen",
|
||||
"Recraft": "Recraft",
|
||||
"edit_models": "edit_models",
|
||||
"Rodin": "Rodin",
|
||||
"Runway": "Runway",
|
||||
"animation": "animation",
|
||||
"api": "api",
|
||||
"save": "save",
|
||||
"upscale_diffusion": "upscale_diffusion",
|
||||
"clip": "clip",
|
||||
"Stability AI": "Stability AI",
|
||||
"stable_cascade": "stable_cascade",
|
||||
"3d_models": "3d_models",
|
||||
"style_model": "style_model",
|
||||
"sd": "sd",
|
||||
"Veo": "Veo"
|
||||
"Tripo": "Tripo",
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"camera": "camera",
|
||||
"Wan": "Wan"
|
||||
},
|
||||
"dataTypes": {
|
||||
"*": "*",
|
||||
"AUDIO": "AUDIO",
|
||||
"AUDIO_ENCODER": "AUDIO_ENCODER",
|
||||
"AUDIO_ENCODER_OUTPUT": "AUDIO_ENCODER_OUTPUT",
|
||||
"AUDIO_RECORD": "AUDIO_RECORD",
|
||||
"BOOLEAN": "BOOLEAN",
|
||||
"CAMERA_CONTROL": "CAMERA_CONTROL",
|
||||
"CLIP": "CLIP",
|
||||
@@ -1468,6 +1508,7 @@
|
||||
"CONTROL_NET": "CONTROL_NET",
|
||||
"FLOAT": "FLOAT",
|
||||
"FLOATS": "FLOATS",
|
||||
"GEMINI_INPUT_FILES": "GEMINI_INPUT_FILES",
|
||||
"GLIGEN": "GLIGEN",
|
||||
"GUIDER": "GUIDER",
|
||||
"HOOK_KEYFRAMES": "HOOK_KEYFRAMES",
|
||||
@@ -1479,17 +1520,25 @@
|
||||
"LOAD_3D": "LOAD_3D",
|
||||
"LOAD_3D_ANIMATION": "LOAD_3D_ANIMATION",
|
||||
"LOAD3D_CAMERA": "LOAD3D_CAMERA",
|
||||
"LORA_MODEL": "LORA_MODEL",
|
||||
"LOSS_MAP": "LOSS_MAP",
|
||||
"LUMA_CONCEPTS": "LUMA_CONCEPTS",
|
||||
"LUMA_REF": "LUMA_REF",
|
||||
"MASK": "MASK",
|
||||
"MESH": "MESH",
|
||||
"MODEL": "MODEL",
|
||||
"MODEL_PATCH": "MODEL_PATCH",
|
||||
"MODEL_TASK_ID": "MODEL_TASK_ID",
|
||||
"NOISE": "NOISE",
|
||||
"OPENAI_CHAT_CONFIG": "OPENAI_CHAT_CONFIG",
|
||||
"OPENAI_INPUT_FILES": "OPENAI_INPUT_FILES",
|
||||
"PHOTOMAKER": "PHOTOMAKER",
|
||||
"PIXVERSE_TEMPLATE": "PIXVERSE_TEMPLATE",
|
||||
"RECRAFT_COLOR": "RECRAFT_COLOR",
|
||||
"RECRAFT_CONTROLS": "RECRAFT_CONTROLS",
|
||||
"RECRAFT_V3_STYLE": "RECRAFT_V3_STYLE",
|
||||
"RETARGET_TASK_ID": "RETARGET_TASK_ID",
|
||||
"RIG_TASK_ID": "RIG_TASK_ID",
|
||||
"SAMPLER": "SAMPLER",
|
||||
"SIGMAS": "SIGMAS",
|
||||
"STRING": "STRING",
|
||||
@@ -1500,6 +1549,7 @@
|
||||
"VAE": "VAE",
|
||||
"VIDEO": "VIDEO",
|
||||
"VOXEL": "VOXEL",
|
||||
"WAN_CAMERA_EMBEDDING": "WAN_CAMERA_EMBEDDING",
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -1862,5 +1912,33 @@
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
},
|
||||
"assetBrowser": {
|
||||
"assets": "Assets",
|
||||
"browseAssets": "Browse Assets",
|
||||
"noAssetsFound": "No assets found",
|
||||
"tryAdjustingFilters": "Try adjusting your search or filters",
|
||||
"loadingModels": "Loading {type}...",
|
||||
"connectionError": "Please check your connection and try again",
|
||||
"noModelsInFolder": "No {type} available in this folder",
|
||||
"searchAssetsPlaceholder": "Search assets...",
|
||||
"allModels": "All Models",
|
||||
"unknown": "Unknown",
|
||||
"fileFormats": "File formats",
|
||||
"baseModels": "Base models",
|
||||
"sortBy": "Sort by",
|
||||
"sortAZ": "A-Z",
|
||||
"sortZA": "Z-A",
|
||||
"sortRecent": "Recent",
|
||||
"sortPopular": "Popular"
|
||||
},
|
||||
"desktopDialogs": {
|
||||
"": {
|
||||
"title": "Invalid Dialog",
|
||||
"message": "Invalid dialog ID was provided.",
|
||||
"buttons": {
|
||||
"Close": "Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,34 @@
|
||||
"custom": "custom"
|
||||
}
|
||||
},
|
||||
"Comfy_Assets_UseAssetAPI": {
|
||||
"name": "Use Asset API for model library",
|
||||
"tooltip": "Use new Asset API for model browsing"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "Canvas background image",
|
||||
"tooltip": "Image URL for the canvas background. You can right-click an image in the outputs panel and select \"Set as Background\" to use it, or upload your own image using the upload button."
|
||||
},
|
||||
"Comfy_Canvas_LeftMouseClickBehavior": {
|
||||
"name": "Left Mouse Click Behavior",
|
||||
"options": {
|
||||
"Panning": "Panning",
|
||||
"Select": "Select"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_MouseWheelScroll": {
|
||||
"name": "Mouse Wheel Scroll",
|
||||
"options": {
|
||||
"Panning": "Panning",
|
||||
"Zoom in/out": "Zoom in/out"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "Navigation Mode",
|
||||
"options": {
|
||||
"Standard (New)": "Standard (New)",
|
||||
"Drag Navigation": "Drag Navigation"
|
||||
"Drag Navigation": "Drag Navigation",
|
||||
"Custom": "Custom"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
@@ -343,14 +362,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "Validate workflows"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Enable Vue node rendering",
|
||||
"tooltip": "Render nodes as Vue components instead of canvas elements. Experimental feature."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Enable Vue widgets",
|
||||
"tooltip": "Render widgets as Vue components within Vue nodes."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Widget control mode",
|
||||
"tooltip": "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
|
||||
@@ -388,6 +399,9 @@
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "Sort node IDs when saving workflow"
|
||||
},
|
||||
"Comfy_Workflow_WarnBlueprintOverwrite": {
|
||||
"name": "Require confirmation to overwrite an existing subgraph blueprint"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "Opened workflows position",
|
||||
"options": {
|
||||
@@ -396,14 +410,14 @@
|
||||
"Topbar (2nd-row)": "Topbar (2nd-row)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MinFontSizeForLOD": {
|
||||
"name": "Zoom Node Level of Detail - font size threshold",
|
||||
"tooltip": "Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer."
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Maximum FPS",
|
||||
"tooltip": "The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0"
|
||||
},
|
||||
"LiteGraph_Canvas_MinFontSizeForLOD": {
|
||||
"name": "Zoom Node Level of Detail - font size threshold",
|
||||
"tooltip": "Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer."
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Scale node combo widget menus (lists) when zoomed in"
|
||||
},
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Exportar flujo de trabajo (formato API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Dar retroalimentación"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir selección en subgrafo"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "Alternar Barra Lateral de Flujos de Trabajo",
|
||||
"tooltip": "Flujos de Trabajo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "Retroalimentación",
|
||||
"filter": "Filtrar",
|
||||
"findIssues": "Encontrar problemas",
|
||||
"firstTimeUIMessage": "Esta es la primera vez que usas la nueva interfaz. Elige \"Menú > Usar nuevo menú > Desactivado\" para restaurar la antigua interfaz.",
|
||||
"frontendNewer": "La versión del frontend {frontendVersion} puede no ser compatible con la versión del backend {backendVersion}.",
|
||||
"frontendOutdated": "La versión del frontend {frontendVersion} está desactualizada. El backend requiere la versión {requiredVersion} o superior.",
|
||||
"goToNode": "Ir al nodo",
|
||||
"help": "Ayuda",
|
||||
"icon": "Icono",
|
||||
"imageFailedToLoad": "Falló la carga de la imagen",
|
||||
"imageUrl": "URL de la imagen",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "Panel inferior",
|
||||
"Browse Templates": "Explorar plantillas",
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||
"Check for Custom Node Updates": "Buscar actualizaciones de nodos personalizados",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
|
||||
@@ -808,18 +802,11 @@
|
||||
"Show Settings Dialog": "Mostrar diálogo de configuración",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
|
||||
"Toggle Search Box": "Alternar caja de búsqueda",
|
||||
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
|
||||
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
|
||||
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
|
||||
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
@@ -828,7 +815,6 @@
|
||||
"Unlock Canvas": "Desbloquear lienzo",
|
||||
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
|
||||
"View": "Ver",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar",
|
||||
@@ -839,11 +825,7 @@
|
||||
"renderBypassState": "Mostrar estado de omisión",
|
||||
"renderErrorState": "Mostrar estado de error",
|
||||
"showGroups": "Mostrar marcos/grupos",
|
||||
"showLinks": "Mostrar enlaces",
|
||||
"sideToolbar_modelLibrary": "sideToolbar.bibliotecaDeModelos",
|
||||
"sideToolbar_nodeLibrary": "sideToolbar.bibliotecaDeNodos",
|
||||
"sideToolbar_queue": "sideToolbar.cola",
|
||||
"sideToolbar_workflows": "sideToolbar.flujosDeTrabajo"
|
||||
"showLinks": "Mostrar enlaces"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
@@ -920,9 +902,6 @@
|
||||
"upscale_diffusion": "difusión_de_escalado",
|
||||
"upscaling": "escalado",
|
||||
"utils": "utilidades",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "video",
|
||||
"video_models": "modelos_de_video"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "Guardar archivos SVG en el disco.",
|
||||
"display_name": "Guardar SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "prefijo_de_archivo",
|
||||
"tooltip": "El prefijo para el archivo a guardar. Esto puede incluir información de formato como %date:yyyy-MM-dd% o %Empty Latent Image.width% para incluir valores de los nodos."
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "Guarda las imágenes de entrada en tu directorio de salida de ComfyUI.",
|
||||
"display_name": "Guardar video",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "Validar flujos de trabajo"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Habilitar renderizado de nodos Vue",
|
||||
"tooltip": "Renderiza los nodos como componentes Vue en lugar de elementos canvas. Función experimental."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Habilitar widgets de Vue",
|
||||
"tooltip": "Renderiza los widgets como componentes de Vue dentro de los nodos de Vue."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Modo de control del widget",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "Barra superior (2ª fila)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Umbral de renderizado de baja calidad al hacer zoom",
|
||||
"tooltip": "Renderiza formas de baja calidad cuando se aleja"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "FPS máximo",
|
||||
"tooltip": "La cantidad máxima de cuadros por segundo que se permite renderizar en el lienzo. Limita el uso de la GPU a costa de la suavidad. Si es 0, se utiliza la tasa de refresco de la pantalla. Predeterminado: 0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Siempre ajustar a la cuadrícula"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Exporter le flux de travail (format API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Retour d'information"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir la sélection en sous-graphe"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "Basculer la barre latérale des flux de travail",
|
||||
"tooltip": "Flux de travail"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "Commentaires",
|
||||
"filter": "Filtrer",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
"frontendNewer": "La version du frontend {frontendVersion} peut ne pas être compatible avec la version du backend {backendVersion}.",
|
||||
"frontendOutdated": "La version du frontend {frontendVersion} est obsolète. Le backend requiert la version {requiredVersion} ou supérieure.",
|
||||
"goToNode": "Aller au nœud",
|
||||
"help": "Aide",
|
||||
"icon": "Icône",
|
||||
"imageFailedToLoad": "Échec du chargement de l'image",
|
||||
"imageUrl": "URL de l'image",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "Panneau inférieur",
|
||||
"Browse Templates": "Parcourir les modèles",
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
|
||||
"Check for Custom Node Updates": "Vérifier les mises à jour des nœuds personnalisés",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
|
||||
@@ -808,18 +802,11 @@
|
||||
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Toggle Essential Bottom Panel": "Basculer le panneau inférieur essentiel",
|
||||
"Toggle Bottom Panel": "Basculer le panneau inférieur",
|
||||
"Toggle Focus Mode": "Basculer le mode focus",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file d’attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
|
||||
"Toggle View Controls Bottom Panel": "Basculer le panneau inférieur des contrôles d’affichage",
|
||||
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
|
||||
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
@@ -828,7 +815,6 @@
|
||||
"Unlock Canvas": "Déverrouiller le canevas",
|
||||
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
|
||||
"View": "Afficher",
|
||||
"Workflow": "Flux de travail",
|
||||
"Workflows": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
@@ -839,12 +825,7 @@
|
||||
"renderBypassState": "Afficher l'état de contournement",
|
||||
"renderErrorState": "Afficher l'état d'erreur",
|
||||
"showGroups": "Afficher les cadres/groupes",
|
||||
"showLinks": "Afficher les liens",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"sideToolbar_modelLibrary": "Bibliothèque de modèles",
|
||||
"sideToolbar_nodeLibrary": "Bibliothèque de nœuds",
|
||||
"sideToolbar_queue": "File d'attente",
|
||||
"sideToolbar_workflows": "Flux de travail"
|
||||
"showLinks": "Afficher les liens"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
@@ -921,9 +902,6 @@
|
||||
"upscale_diffusion": "diffusion_de_mise_à_l'échelle",
|
||||
"upscaling": "mise_à_l'échelle",
|
||||
"utils": "utilitaires",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "vidéo",
|
||||
"video_models": "modèles_vidéo"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "Enregistrer les fichiers SVG sur le disque.",
|
||||
"display_name": "Enregistrer SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "préfixe_nom_fichier",
|
||||
"tooltip": "Le préfixe pour le fichier à enregistrer. Cela peut inclure des informations de formatage telles que %date:yyyy-MM-dd% ou %Empty Latent Image.width% pour inclure des valeurs provenant des nœuds."
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "Enregistre les images d'entrée dans votre répertoire de sortie ComfyUI.",
|
||||
"display_name": "Enregistrer la vidéo",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "Valider les flux de travail"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Activer le rendu des nœuds Vue",
|
||||
"tooltip": "Rendre les nœuds comme composants Vue au lieu d’éléments canvas. Fonctionnalité expérimentale."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Activer les widgets Vue",
|
||||
"tooltip": "Rendre les widgets comme composants Vue à l'intérieur des nœuds Vue."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Mode de contrôle du widget",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "Barre supérieure (2ème rangée)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Seuil de zoom pour le rendu de faible qualité",
|
||||
"tooltip": "Rendre des formes de faible qualité lorsqu'on est dézoomé"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "FPS maximum",
|
||||
"tooltip": "Le nombre maximum d'images par seconde que le canevas est autorisé à rendre. Limite l'utilisation du GPU au détriment de la fluidité. Si 0, le taux de rafraîchissement de l'écran est utilisé. Par défaut : 0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Toujours aligner sur la grille"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "ワークフローをエクスポート(API形式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "フィードバック"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "選択範囲をサブグラフに変換"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "ワークフローサイドバーの切り替え",
|
||||
"tooltip": "ワークフロー"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "フィードバック",
|
||||
"filter": "フィルタ",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択することで古いUIに戻すことが可能です。",
|
||||
"frontendNewer": "フロントエンドのバージョン {frontendVersion} はバックエンドのバージョン {backendVersion} と互換性がない可能性があります。",
|
||||
"frontendOutdated": "フロントエンドのバージョン {frontendVersion} は古くなっています。バックエンドは {requiredVersion} 以上が必要です。",
|
||||
"goToNode": "ノードに移動",
|
||||
"help": "ヘルプ",
|
||||
"icon": "アイコン",
|
||||
"imageFailedToLoad": "画像の読み込みに失敗しました",
|
||||
"imageUrl": "画像URL",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "下部パネル",
|
||||
"Browse Templates": "テンプレートを参照",
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||
"Check for Custom Node Updates": "カスタムノードのアップデートを確認",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
"Fit view to selected nodes": "選択したノードにビューを合わせる",
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
|
||||
@@ -809,16 +803,9 @@
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログ下部パネルの切り替え",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
|
||||
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
|
||||
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナル下部パネルの切り替え",
|
||||
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
|
||||
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
|
||||
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
@@ -827,7 +814,6 @@
|
||||
"Unlock Canvas": "キャンバスのロックを解除",
|
||||
"Unpack the selected Subgraph": "選択したサブグラフを展開",
|
||||
"View": "表示",
|
||||
"Workflow": "ワークフロー",
|
||||
"Workflows": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト",
|
||||
@@ -838,11 +824,7 @@
|
||||
"renderBypassState": "バイパス状態を表示",
|
||||
"renderErrorState": "エラー状態を表示",
|
||||
"showGroups": "フレーム/グループを表示",
|
||||
"showLinks": "リンクを表示",
|
||||
"sideToolbar_modelLibrary": "モデルライブラリ",
|
||||
"sideToolbar_nodeLibrary": "ノードライブラリ",
|
||||
"sideToolbar_queue": "キュー",
|
||||
"sideToolbar_workflows": "ワークフロー"
|
||||
"showLinks": "リンクを表示"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
@@ -919,9 +901,6 @@
|
||||
"upscale_diffusion": "アップスケール拡散",
|
||||
"upscaling": "アップスケーリング",
|
||||
"utils": "ユーティリティ",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "ビデオ",
|
||||
"video_models": "ビデオモデル"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "SVGファイルをディスクに保存します。",
|
||||
"display_name": "SVGを保存",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "ファイル名プレフィックス",
|
||||
"tooltip": "保存するファイルのプレフィックスです。%date:yyyy-MM-dd% や %Empty Latent Image.width% など、ノードからの値を含めるフォーマット情報を指定できます。"
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "入力画像をComfyUIの出力ディレクトリに保存します。",
|
||||
"display_name": "ビデオを保存",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "ワークフローを検証"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Vueノードレンダリングを有効化",
|
||||
"tooltip": "ノードをキャンバス要素の代わりにVueコンポーネントとしてレンダリングします。実験的な機能です。"
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Vueウィジェットを有効化",
|
||||
"tooltip": "ウィジェットをVueノード内のVueコンポーネントとしてレンダリングします。"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "ウィジェット制御モード",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "トップバー(2行目)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低品質レンダリングズーム閾値",
|
||||
"tooltip": "ズームアウト時に低品質の形状をレンダリングする"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "キャンバスがレンダリングできる最大フレーム数です。スムーズさの代わりにGPU使用量を制限します。0の場合、画面のリフレッシュレートが使用されます。デフォルト:0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "常にグリッドにスナップ"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "워크플로 내보내기 (API 형식)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "피드백"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "선택 영역을 서브그래프로 변환"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "워크플로 사이드바 토글",
|
||||
"tooltip": "워크플로"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "피드백",
|
||||
"filter": "필터",
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래된 버전입니다. 백엔드는 {requiredVersion} 이상 버전이 필요합니다.",
|
||||
"goToNode": "노드로 이동",
|
||||
"help": "도움말",
|
||||
"icon": "아이콘",
|
||||
"imageFailedToLoad": "이미지를 로드하지 못했습니다.",
|
||||
"imageUrl": "이미지 URL",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "하단 패널",
|
||||
"Browse Templates": "템플릿 탐색",
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||
"Check for Custom Node Updates": "커스텀 노드 업데이트 확인",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
|
||||
@@ -808,20 +802,11 @@
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "대기열 사이드바 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
@@ -830,7 +815,6 @@
|
||||
"Unlock Canvas": "캔버스 잠금 해제",
|
||||
"Unpack the selected Subgraph": "선택한 서브그래프 묶음 풀기",
|
||||
"View": "보기",
|
||||
"Workflow": "워크플로",
|
||||
"Workflows": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
@@ -841,11 +825,7 @@
|
||||
"renderBypassState": "바이패스 상태 렌더링",
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시",
|
||||
"sideToolbar_modelLibrary": "사이드툴바.모델 라이브러리",
|
||||
"sideToolbar_nodeLibrary": "사이드툴바.노드 라이브러리",
|
||||
"sideToolbar_queue": "사이드툴바.대기열",
|
||||
"sideToolbar_workflows": "사이드툴바.워크플로"
|
||||
"showLinks": "링크 표시"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
@@ -922,9 +902,6 @@
|
||||
"upscale_diffusion": "업스케일 확산",
|
||||
"upscaling": "업스케일링",
|
||||
"utils": "유틸리티",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "비디오",
|
||||
"video_models": "비디오 모델"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "SVG 파일을 디스크에 저장합니다.",
|
||||
"display_name": "SVG 저장",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "파일명 접두사",
|
||||
"tooltip": "저장할 파일의 접두사입니다. %date:yyyy-MM-dd% 또는 %Empty Latent Image.width%와 같이 노드의 값을 포함하는 형식 정보를 사용할 수 있습니다."
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "입력 이미지를 ComfyUI 출력 디렉토리에 저장합니다.",
|
||||
"display_name": "비디오 저장",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "워크플로 유효성 검사"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Vue 노드 렌더링 활성화",
|
||||
"tooltip": "노드를 캔버스 요소 대신 Vue 컴포넌트로 렌더링합니다. 실험적인 기능입니다."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Vue 위젯 활성화",
|
||||
"tooltip": "Vue 노드 내에서 위젯을 Vue 컴포넌트로 렌더링합니다."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "위젯 제어 모드",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "상단바 (2번째 행)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "저품질 렌더링 줌 임계값",
|
||||
"tooltip": "줌 아웃시 저품질 도형 렌더링"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "최대 FPS",
|
||||
"tooltip": "캔버스가 렌더링할 수 있는 최대 프레임 수입니다. 부드럽게 동작하도록 GPU 사용률을 제한 합니다. 0이면 화면 주사율로 작동 합니다. 기본값: 0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "항상 그리드에 스냅"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Экспорт рабочего процесса (формат API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Обратная связь"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Преобразовать выделенное в подграф"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "Переключить боковую панель рабочих процессов",
|
||||
"tooltip": "Рабочие процессы"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "Обратная связь",
|
||||
"filter": "Фильтр",
|
||||
"findIssues": "Найти проблемы",
|
||||
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
|
||||
"frontendNewer": "Версия интерфейса {frontendVersion} может быть несовместима с версией сервера {backendVersion}.",
|
||||
"frontendOutdated": "Версия интерфейса {frontendVersion} устарела. Требуется версия не ниже {requiredVersion} для работы с сервером.",
|
||||
"goToNode": "Перейти к ноде",
|
||||
"help": "Помощь",
|
||||
"icon": "Иконка",
|
||||
"imageFailedToLoad": "Не удалось загрузить изображение",
|
||||
"imageUrl": "URL изображения",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "Нижняя панель",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
|
||||
"Check for Custom Node Updates": "Проверить обновления пользовательских узлов",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
|
||||
@@ -809,17 +803,10 @@
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Essential Bottom Panel": "Показать/скрыть нижнюю панель основных элементов",
|
||||
"Toggle Logs Bottom Panel": "Показать/скрыть нижнюю панель логов",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
|
||||
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Показать/скрыть нижнюю панель терминала",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
|
||||
"Toggle View Controls Bottom Panel": "Показать/скрыть нижнюю панель элементов управления",
|
||||
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
|
||||
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
@@ -828,7 +815,6 @@
|
||||
"Unlock Canvas": "Разблокировать холст",
|
||||
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
|
||||
"View": "Вид",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
@@ -839,11 +825,7 @@
|
||||
"renderBypassState": "Отображать состояние обхода",
|
||||
"renderErrorState": "Отображать состояние ошибки",
|
||||
"showGroups": "Показать фреймы/группы",
|
||||
"showLinks": "Показать связи",
|
||||
"sideToolbar_modelLibrary": "sideToolbar.каталогМоделей",
|
||||
"sideToolbar_nodeLibrary": "sideToolbar.каталогУзлов",
|
||||
"sideToolbar_queue": "sideToolbar.очередь",
|
||||
"sideToolbar_workflows": "sideToolbar.рабочиеПроцессы"
|
||||
"showLinks": "Показать связи"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
@@ -920,9 +902,6 @@
|
||||
"upscale_diffusion": "диффузии_апскейла",
|
||||
"upscaling": "апскейл",
|
||||
"utils": "утилиты",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "видео",
|
||||
"video_models": "видеомодели"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "Сохранять файлы SVG на диск.",
|
||||
"display_name": "Сохранить SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "префикс_имени_файла",
|
||||
"tooltip": "Префикс для сохраняемого файла. Может включать информацию о форматировании, такую как %date:yyyy-MM-dd% или %Empty Latent Image.width% для включения значений из узлов."
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "Сохраняет входные изображения в вашу папку вывода ComfyUI.",
|
||||
"display_name": "Сохранить видео",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "Проверка рабочих процессов"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Включить рендеринг узлов через Vue",
|
||||
"tooltip": "Отображать узлы как компоненты Vue вместо элементов canvas. Экспериментальная функция."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Включить виджеты Vue",
|
||||
"tooltip": "Отображать виджеты как компоненты Vue внутри узлов Vue."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Режим управления виджетом",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "Топбар (2-й ряд)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Порог масштабирования для рендеринга низкого качества",
|
||||
"tooltip": "Рендеринг фигур низкого качества при уменьшении масштаба"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Максимум FPS",
|
||||
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Всегда привязываться к сетке"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
309
src/locales/tr/commands.json
Normal file
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Güncellemeleri Kontrol Et"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Özel Düğümler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Girişler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Kayıtlar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "extra_model_paths.yaml dosyasını aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Modeller Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Çıktılar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Geliştirici Araçlarını Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Masaüstü Kullanıcı Kılavuzu"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Çık"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Yeniden Yükle"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Yeniden Başlat"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Şablonlara Gözat"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Seçili Öğeleri Sil"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Görünümü seçili düğümlere sığdır"
|
||||
},
|
||||
"Comfy_Canvas_Lock": {
|
||||
"label": "Tuvali Kilitle"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Seçili Düğümleri Aşağı Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Seçili Düğümleri Sola Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Seçili Düğümleri Sağa Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Seçili Düğümleri Yukarı Taşı"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Görünümü Sıfırla"
|
||||
},
|
||||
"Comfy_Canvas_Resize": {
|
||||
"label": "Seçili Düğümleri Yeniden Boyutlandır"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLinkVisibility": {
|
||||
"label": "Tuval Bağlantı Görünürlüğünü Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Tuval Kilidini Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Mini Haritayı Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Seçili Düğümleri Atla/Geri Al"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "Seçili Düğümleri Daralt/Genişlet"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "Seçili Düğümleri Sessize Al/Sesi Aç"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "Seçili Düğümleri Sabitle/Sabitlemeyi Kaldır"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "Seçili Öğeleri Sabitle/Sabitlemeyi Kaldır"
|
||||
},
|
||||
"Comfy_Canvas_Unlock": {
|
||||
"label": "Tuvalin Kilidini Aç"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "Yakınlaştır"
|
||||
},
|
||||
"Comfy_Canvas_ZoomOut": {
|
||||
"label": "Uzaklaştır"
|
||||
},
|
||||
"Comfy_ClearPendingTasks": {
|
||||
"label": "Bekleyen Görevleri Temizle"
|
||||
},
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "İş Akışını Temizle"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Destekle İletişime Geç"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Model Seçiciyi Göster (Geliştirici)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Mevcut İş Akışını Çoğalt"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "İş Akışını Dışa Aktar"
|
||||
},
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "İş Akışını Dışa Aktar (API Formatı)"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Seçimi Alt Grafiğe Dönüştür"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Alt Grafikten Çık"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Grubu İçeriğe Sığdır"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Seçili Düğümleri Gruplandır"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Seçili Alt Grafiği Aç"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Seçili düğümleri grup düğümüne dönüştür"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "Grup düğümlerini yönet"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "Seçili grup düğümlerinin grubunu çöz"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "ComfyUI Hakkında'yı Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "Comfy-Org Discord'unu Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "ComfyUI Belgelerini Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "ComfyUI Forumunu Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "ComfyUI Sorunlarını Aç"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "Kes"
|
||||
},
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Varsayılan İş Akışını Yükle"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Özel Düğüm Yöneticisi"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Özel Düğümler (Eski)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Yönetici Menüsü (Eski)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Eksik Özel Düğümleri Yükle"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Maske Düzenleyicide Fırça Boyutunu Artır"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Modelleri Boşalt"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Modelleri ve Yürütme Önbelleğini Boşalt"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Yeni Boş İş Akışı"
|
||||
},
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Yönetici"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "İş Akışını Aç"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Alt Grafiği Yayınla"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "İstemi Kuyruğa Al"
|
||||
},
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "İstemi Kuyruğa Al (Ön)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Seçili Çıktı Düğümlerini Kuyruğa Al"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Yinele"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Düğüm Tanımlarını Yenile"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "İş Akışını Kaydet"
|
||||
},
|
||||
"Comfy_SaveWorkflowAs": {
|
||||
"label": "İş Akışını Farklı Kaydet"
|
||||
},
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Ayarlar İletişim Kutusunu Göster"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Tuval Performansı"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Yardım Merkezi"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Temayı Değiştir (Karanlık/Açık)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "Geri Al"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Giriş Yapma İletişim Kutusunu Aç"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Çıkış Yap"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Mevcut İş Akışını Kapat"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "Sonraki Açılan İş Akışı"
|
||||
},
|
||||
"Workspace_PreviousOpenedWorkflow": {
|
||||
"label": "Önceki Açılan İş Akışı"
|
||||
},
|
||||
"Workspace_SearchBox_Toggle": {
|
||||
"label": "Arama Kutusunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "Alt Paneli Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "Terminal Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "Kayıtlar Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "Temel Alt Paneli Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "Görünüm Kontrolleri Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "Tuş Atamaları İletişim Kutusunu Göster"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Odak Modunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Model Kütüphanesi"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Düğüm Kütüphanesi"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Kuyruk"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "İş Akışları"
|
||||
}
|
||||
}
|
||||
1780
src/locales/tr/main.json
Normal file
8647
src/locales/tr/nodeDefs.json
Normal file
416
src/locales/tr/settings.json
Normal file
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"Comfy-Desktop_AutoUpdate": {
|
||||
"name": "Güncellemeleri otomatik olarak kontrol et"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Anonim kullanım metrikleri gönder"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi Yükleme Yansısı",
|
||||
"tooltip": "Varsayılan pip yükleme yansısı"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python Yükleme Yansısı",
|
||||
"tooltip": "Yönetilen Python kurulumları Astral python-build-standalone projesinden indirilir. Bu değişken, Python kurulumları için farklı bir kaynak kullanmak üzere bir yansıma URL'sine ayarlanabilir. Sağlanan URL, örneğin https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'deki https://github.com/astral-sh/python-build-standalone/releases/download'ın yerini alacaktır. Dağıtımlar, file:// URL şeması kullanılarak yerel bir dizinden okunabilir."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch Yükleme Yansısı",
|
||||
"tooltip": "Pytorch için Pip yükleme yansısı"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Pencere Stili",
|
||||
"options": {
|
||||
"custom": "özel",
|
||||
"default": "varsayılan"
|
||||
},
|
||||
"tooltip": "Özel: Sistem başlık çubuğunu ComfyUI'nin Üst menüsüyle değiştirin"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "Tuval arka plan resmi",
|
||||
"tooltip": "Tuval arka planı için resim URL'si. Çıktılar panelindeki bir resme sağ tıklayıp \"Arka Plan Olarak Ayarla\"yı seçerek kullanabilir veya yükleme düğmesini kullanarak kendi resminizi yükleyebilirsiniz."
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "Tuval Gezinme Modu",
|
||||
"options": {
|
||||
"Drag Navigation": "Sürükleyerek Gezinme",
|
||||
"Standard (New)": "Standart (Yeni)"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "Seçim araç kutusunu göster"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "İş akışını temizlerken onay iste"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "DOM öğesi kırpmayı etkinleştir (etkinleştirmek performansı düşürebilir)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "Geliştirici modu seçeneklerini etkinleştir (API kaydetme, vb.)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "Varsayılan ondalık sayı widget yuvarlamasını devre dışı bırak.",
|
||||
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir) Arka uçtaki düğüm tarafından yuvarlama ayarlandığında yuvarlama devre dışı bırakılamaz."
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "Düğüm widget kaydırıcılarını devre dışı bırak"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "Ctrl+yukarı/aşağı hassasiyeti"
|
||||
},
|
||||
"Comfy_EnableTooltips": {
|
||||
"name": "Araç İpuçlarını Etkinleştir"
|
||||
},
|
||||
"Comfy_EnableWorkflowViewRestore": {
|
||||
"name": "İş akışlarında tuval konumunu ve yakınlaştırma seviyesini kaydet ve geri yükle"
|
||||
},
|
||||
"Comfy_FloatRoundingPrecision": {
|
||||
"name": "Ondalık sayı widget yuvarlama ondalık basamakları [0 = otomatik].",
|
||||
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir)"
|
||||
},
|
||||
"Comfy_Graph_CanvasInfo": {
|
||||
"name": "Sol alt köşede tuval bilgilerini göster (fps, vb.)"
|
||||
},
|
||||
"Comfy_Graph_CanvasMenu": {
|
||||
"name": "Grafik tuval menüsünü göster"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Bağlantı orta nokta işaretçileri",
|
||||
"options": {
|
||||
"Arrow": "Ok",
|
||||
"Circle": "Daire",
|
||||
"None": "Yok"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "Tuval yakınlaştırma hızı"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "Seçili düğümleri gruplandırma dolgusu"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "Düzenlemek için grup başlığına çift tıkla"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "Bağlantı bırakıldığında eylem (Değiştirici yok)",
|
||||
"options": {
|
||||
"context menu": "bağlam menüsü",
|
||||
"no action": "eylem yok",
|
||||
"search box": "arama kutusu"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "Bağlantı bırakıldığında eylem (Shift)",
|
||||
"options": {
|
||||
"context menu": "bağlam menüsü",
|
||||
"no action": "eylem yok",
|
||||
"search box": "arama kutusu"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "Bağlantı Oluşturma Modu",
|
||||
"options": {
|
||||
"Hidden": "Gizli",
|
||||
"Linear": "Doğrusal",
|
||||
"Spline": "Eğri",
|
||||
"Straight": "Düz"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "3D Görüntüleyiciyi Etkinleştir (Beta)",
|
||||
"tooltip": "Seçili düğümler için 3D Görüntüleyiciyi (Beta) etkinleştirir. Bu özellik, 3D modelleri doğrudan tam boyutlu 3D görüntüleyici içinde görselleştirmenize ve etkileşimde bulunmanıza olanak tanır."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Başlangıç Arka Plan Rengi",
|
||||
"tooltip": "3D sahnenin varsayılan arka plan rengini kontrol eder. Bu ayar, yeni bir 3D widget oluşturulduğunda arka plan görünümünü belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Başlangıç Kamera Tipi",
|
||||
"options": {
|
||||
"orthographic": "ortografik",
|
||||
"perspective": "perspektif"
|
||||
},
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda kameranın varsayılan olarak perspektif mi yoksa ortografik mi olacağını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Işık Ayarlama Artışı",
|
||||
"tooltip": "3D sahnelerde ışık yoğunluğunu ayarlarken artış boyutunu kontrol eder. Daha küçük bir adım değeri, aydınlatma ayarlamaları üzerinde daha ince kontrol sağlarken, daha büyük bir değer ayarlama başına daha belirgin değişikliklere neden olur."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Başlangıç Işık Yoğunluğu",
|
||||
"tooltip": "3D sahnedeki aydınlatmanın varsayılan parlaklık seviyesini ayarlar. Bu değer, yeni bir 3D widget oluşturulduğunda ışıkların nesneleri ne kadar yoğun aydınlatacağını belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Maksimum Işık Yoğunluğu",
|
||||
"tooltip": "3D sahneler için izin verilen maksimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek üst parlaklık sınırını tanımlar."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Minimum Işık Yoğunluğu",
|
||||
"tooltip": "3D sahneler için izin verilen minimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek alt parlaklık sınırını tanımlar."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Başlangıç Izgara Görünürlüğü",
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda ızgaranın varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Başlangıç Önizleme Görünürlüğü",
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda önizleme ekranının varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Dil"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "Fırça ayar hızı çarpanı",
|
||||
"tooltip": "Ayarlama sırasında fırça boyutunun ve sertliğinin ne kadar hızlı değiştiğini kontrol eder. Daha yüksek değerler daha hızlı değişiklikler anlamına gelir."
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "Fırça ayarını baskın eksene kilitle",
|
||||
"tooltip": "Etkinleştirildiğinde, fırça ayarları yalnızca daha fazla hareket ettiğiniz yöne bağlı olarak boyutu VEYA sertliği etkileyecektir"
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "Yeni maske düzenleyiciyi kullan",
|
||||
"tooltip": "Yeni maske düzenleyici arayüzüne geç"
|
||||
},
|
||||
"Comfy_ModelLibrary_AutoLoadAll": {
|
||||
"name": "Tüm model klasörlerini otomatik olarak yükle",
|
||||
"tooltip": "Doğruysa, model kütüphanesini açar açmaz tüm klasörler yüklenecektir (bu, yüklenirken gecikmelere neden olabilir). Yanlışsa, kök düzeyindeki model klasörleri yalnızca üzerlerine tıkladığınızda yüklenecektir."
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "Model kütüphanesi ağaç görünümünde hangi adın görüntüleneceği",
|
||||
"options": {
|
||||
"filename": "dosyaadı",
|
||||
"title": "başlık"
|
||||
},
|
||||
"tooltip": "Model listesinde ham dosya adının (dizin veya \".safetensors\" uzantısı olmadan) basitleştirilmiş bir görünümünü oluşturmak için \"dosyaadı\"nı seçin. Yapılandırılabilir model meta veri başlığını görüntülemek için \"başlık\"ı seçin."
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "Düğüm ID rozeti modu",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "Düğüm yaşam döngüsü rozeti modu",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "Düğüm kaynak rozeti modu",
|
||||
"options": {
|
||||
"Hide built-in": "Yerleşik olanı gizle",
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "API düğüm fiyatlandırma rozetini göster"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Düğüm arama kutusu uygulaması",
|
||||
"options": {
|
||||
"default": "varsayılan",
|
||||
"litegraph (legacy)": "litegraph (eski)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "Düğüm önizlemesi",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "Arama sonuçlarında düğüm kategorisini göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "Arama sonuçlarında düğüm kimliği adını göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "Arama sonuçlarında düğüm sıklığını göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "Düğüm öneri sayısı",
|
||||
"tooltip": "Yalnızca litegraph arama kutusu/bağlam menüsü için"
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "Görüntü önizlemesinin altında genişlik × yüksekliği göster"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "Bağlantıyı otomatik olarak düğüm yuvasına yapıştır",
|
||||
"tooltip": "Bir bağlantıyı bir düğümün üzerine sürüklerken, bağlantı otomatik olarak düğüm üzerindeki uygun bir giriş yuvasına yapışır"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "Düğümleri silerken tüm bağlantıları koru",
|
||||
"tooltip": "Bir düğümü silerken, tüm giriş ve çıkış bağlantılarını yeniden bağlamaya çalışın (silinen düğümü atlayarak)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "Düzenlemek için düğüm başlığına çift tıkla"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "Orta tıklama yeni bir Yeniden Yönlendirme düğümü oluşturur"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "Düğüm opaklığı"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "Aramada kullanımdan kaldırılmış düğümleri göster",
|
||||
"tooltip": "Kullanımdan kaldırılmış düğümler arayüzde varsayılan olarak gizlidir, ancak bunları kullanan mevcut iş akışlarında işlevsel kalır."
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "Aramada deneysel düğümleri göster",
|
||||
"tooltip": "Deneysel düğümler arayüzde bu şekilde işaretlenmiştir ve gelecekteki sürümlerde önemli değişikliklere veya kaldırılmaya tabi olabilir. Üretim iş akışlarında dikkatli kullanın"
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "Yapıştırma düğümü vurgular",
|
||||
"tooltip": "Uygun giriş yuvasına sahip bir düğümün üzerine bir bağlantı sürüklerken, düğümü vurgulayın"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Sürüm güncellemelerini göster",
|
||||
"tooltip": "Yeni modeller ve önemli yeni özellikler için güncellemeleri göster."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "İşaretçi tıklama kayma gecikmesi",
|
||||
"tooltip": "Bir işaretçi düğmesine bastıktan sonra, bu, işaretçi hareketinin göz ardı edilebileceği maksimum süredir (milisaniye cinsinden).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
|
||||
},
|
||||
"Comfy_Pointer_ClickDrift": {
|
||||
"name": "İşaretçi tıklama kayması (maksimum mesafe)",
|
||||
"tooltip": "İşaretçi bir düğmeyi basılı tutarken bu mesafeden daha fazla hareket ederse, bu sürükleme olarak kabul edilir (tıklama yerine).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
|
||||
},
|
||||
"Comfy_Pointer_DoubleClickTime": {
|
||||
"name": "Çift tıklama aralığı (maksimum)",
|
||||
"tooltip": "Çift tıklamanın iki tıklaması arasındaki milisaniye cinsinden maksimum süre. Bu değeri artırmak, çift tıklamaların bazen kaydedilmemesi durumunda yardımcı olabilir."
|
||||
},
|
||||
"Comfy_PreviewFormat": {
|
||||
"name": "Önizleme görüntü formatı",
|
||||
"tooltip": "Görüntü widget'ında bir önizleme görüntülerken, onu hafif bir görüntüye dönüştürün, örn. webp, jpeg, webp;50, vb."
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "İş akışını kaydederken dosya adı iste"
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "Toplu iş sayısı sınırı",
|
||||
"tooltip": "Tek bir düğme tıklamasıyla kuyruğa eklenen maksimum görev sayısı"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "Kuyruk geçmişi boyutu",
|
||||
"tooltip": "Kuyruk geçmişinde gösterilen maksimum görev sayısı."
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "Kenar çubuğu konumu",
|
||||
"options": {
|
||||
"left": "sol",
|
||||
"right": "sağ"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_Size": {
|
||||
"name": "Kenar çubuğu boyutu",
|
||||
"options": {
|
||||
"normal": "normal",
|
||||
"small": "küçük"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Birleşik kenar çubuğu genişliği"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Izgaraya yapıştırma boyutu",
|
||||
"tooltip": "Shift tuşunu basılı tutarken düğümleri sürükleyip yeniden boyutlandırırken ızgaraya hizalanacaklar, bu o ızgaranın boyutunu kontrol eder."
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "Metin alanı widget yazı tipi boyutu"
|
||||
},
|
||||
"Comfy_TextareaWidget_Spellcheck": {
|
||||
"name": "Metin alanı widget yazım denetimi"
|
||||
},
|
||||
"Comfy_TreeExplorer_ItemPadding": {
|
||||
"name": "Ağaç gezgini öğe dolgusu"
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "Yeni menüyü kullan",
|
||||
"options": {
|
||||
"Bottom": "Alt",
|
||||
"Disabled": "Devre dışı",
|
||||
"Top": "Üst"
|
||||
},
|
||||
"tooltip": "Menü çubuğu konumu. Mobil cihazlarda menü her zaman üstte gösterilir."
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "İş akışlarını doğrula"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Widget kontrol modu",
|
||||
"options": {
|
||||
"after": "sonra",
|
||||
"before": "önce"
|
||||
},
|
||||
"tooltip": "Widget değerlerinin ne zaman güncelleneceğini (rastgele/artırma/azaltma), istem kuyruğa alınmadan önce veya sonra kontrol eder."
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "Pencereyi kapatırken onay göster"
|
||||
},
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "Otomatik Kaydet",
|
||||
"options": {
|
||||
"after delay": "gecikmeden sonra",
|
||||
"off": "kapalı"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
"name": "Otomatik Kaydetme Gecikmesi (ms)",
|
||||
"tooltip": "Yalnızca Otomatik Kaydetme \"gecikmeden sonra\" olarak ayarlandığında geçerlidir."
|
||||
},
|
||||
"Comfy_Workflow_ConfirmDelete": {
|
||||
"name": "İş akışlarını silerken onay göster"
|
||||
},
|
||||
"Comfy_Workflow_Persist": {
|
||||
"name": "İş akışı durumunu koru ve sayfayı (yeniden) yüklediğinde geri yükle"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingModelsWarning": {
|
||||
"name": "Eksik model uyarısını göster"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "Eksik düğüm uyarısını göster"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "İş akışını kaydederken düğüm kimliklerini sırala"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "Açılan iş akışları konumu",
|
||||
"options": {
|
||||
"Sidebar": "Kenar Çubuğu",
|
||||
"Topbar": "Üst Çubuk",
|
||||
"Topbar (2nd-row)": "Üst Çubuk (2. sıra)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Maksimum FPS",
|
||||
"tooltip": "Tuvalin saniyede oluşturmasına izin verilen maksimum kare sayısı. Akıcılık pahasına GPU kullanımını sınırlar. 0 ise, ekran yenileme hızı kullanılır. Varsayılan: 0"
|
||||
},
|
||||
"LiteGraph_Canvas_MinFontSizeForLOD": {
|
||||
"name": "Yakınlaştırma Düğümü Ayrıntı Seviyesi - yazı tipi boyutu eşiği",
|
||||
"tooltip": "Düğümlerin ne zaman düşük kaliteli LOD oluşturmaya geçeceğini kontrol eder. Ne zaman geçiş yapılacağını belirlemek için piksel cinsinden yazı tipi boyutunu kullanır. Devre dışı bırakmak için 0'a ayarlayın. 1-24 arasındaki değerler LOD için minimum yazı tipi boyutu eşiğini ayarlar - daha yüksek değerler (24 piksel) = uzaklaştırırken düğümleri daha erken basitleştirilmiş oluşturmaya geçirin, daha düşük değerler (1 piksel) = tam düğüm kalitesini daha uzun süre koruyun."
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Yakınlaştırıldığında düğüm birleşik widget menülerini (listeleri) ölçeklendir"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Yeni düğümleri her zaman küçült",
|
||||
"tooltip": "Oluşturulduğunda düğümleri mümkün olan en küçük boyuta yeniden boyutlandırın. Devre dışı bırakıldığında, yeni eklenen bir düğüm widget değerlerini göstermek için biraz genişletilecektir."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Araç İpucu Gecikmesi"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Yeniden yönlendirme eğri ofseti",
|
||||
"tooltip": "Yeniden yönlendirme merkez noktasından bezier kontrol noktası ofseti"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Her zaman ızgaraya yapıştır"
|
||||
}
|
||||
}
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "匯出工作流程(API 格式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "提供回饋"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "將選取內容轉換為子圖"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "切換工作流程側邊欄",
|
||||
"tooltip": "工作流程"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,11 +324,9 @@
|
||||
"feedback": "意見回饋",
|
||||
"filter": "篩選",
|
||||
"findIssues": "尋找問題",
|
||||
"firstTimeUIMessage": "這是您第一次使用新介面。若要返回舊介面,請前往「選單」>「使用新介面」>「關閉」。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "前往節點",
|
||||
"help": "說明",
|
||||
"icon": "圖示",
|
||||
"imageFailedToLoad": "無法載入圖片",
|
||||
"imageUrl": "圖片網址",
|
||||
@@ -733,9 +731,7 @@
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "瀏覽範本",
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
@@ -760,8 +756,6 @@
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
"Fit view to selected nodes": "視圖貼合選取節點",
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
@@ -813,13 +807,6 @@
|
||||
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
|
||||
"Toggle View Controls Bottom Panel": "切換檢視控制底部面板",
|
||||
"Toggle Bottom Panel": "切換下方面板",
|
||||
"Toggle Focus Mode": "切換專注模式",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
@@ -828,7 +815,6 @@
|
||||
"Unlock Canvas": "解除鎖定畫布",
|
||||
"Unpack the selected Subgraph": "解包所選子圖",
|
||||
"View": "檢視",
|
||||
"Workflow": "工作流程",
|
||||
"Workflows": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
@@ -908,9 +894,6 @@
|
||||
"upscale_diffusion": "擴散放大",
|
||||
"upscaling": "放大",
|
||||
"utils": "工具",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "影片",
|
||||
"video_models": "影片模型"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "將 SVG 檔案儲存到磁碟。",
|
||||
"display_name": "儲存 SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "檔名前綴",
|
||||
"tooltip": "要儲存檔案的字首。這可以包含格式化資訊,例如 %date:yyyy-MM-dd% 或 %Empty Latent Image.width%,以便從節點中包含數值。"
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "將輸入的影像儲存到您的 ComfyUI 輸出目錄。",
|
||||
"display_name": "儲存影片",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "驗證工作流程"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "啟用 Vue 節點渲染",
|
||||
"tooltip": "將節點以 Vue 元件而非畫布元素方式渲染。實驗性功能。"
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "啟用 Vue 小工具",
|
||||
"tooltip": "在 Vue 節點中以 Vue 元件渲染小工具。"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "元件控制模式",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "頂部欄(第二列)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低品質渲染縮放臨界值",
|
||||
"tooltip": "當縮小檢視時以低品質渲染圖形"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "畫布允許渲染的最大每秒幀數。限制GPU使用率,但可能影響流暢度。若設為0,則使用螢幕的更新率。預設值:0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "總是對齊格線"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "导出工作流(API格式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "反馈"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "将选区转换为子图"
|
||||
},
|
||||
@@ -309,4 +306,4 @@
|
||||
"label": "切换工作流侧边栏",
|
||||
"tooltip": "工作流"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +326,6 @@
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能与后端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
"icon": "图标",
|
||||
"imageFailedToLoad": "图像加载失败",
|
||||
"imageUrl": "图片网址",
|
||||
@@ -779,7 +778,6 @@
|
||||
"File": "文件",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
"Focus Mode": "专注模式",
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Help Center": "帮助中心",
|
||||
@@ -833,15 +831,11 @@
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Essential Bottom Panel": "切换基础底部面板",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
|
||||
"Toggle Workflows Sidebar": "切换工作流侧边栏",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
@@ -858,11 +852,7 @@
|
||||
"renderBypassState": "渲染绕过状态",
|
||||
"renderErrorState": "渲染错误状态",
|
||||
"showGroups": "显示框架/分组",
|
||||
"showLinks": "显示连接",
|
||||
"sideToolbar_modelLibrary": "侧边工具栏.模型库",
|
||||
"sideToolbar_nodeLibrary": "侧边工具栏.节点库",
|
||||
"sideToolbar_queue": "侧边工具栏.队列",
|
||||
"sideToolbar_workflows": "侧边工具栏.工作流"
|
||||
"showLinks": "显示连接"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
@@ -939,9 +929,6 @@
|
||||
"upscale_diffusion": "放大扩散",
|
||||
"upscaling": "放大",
|
||||
"utils": "工具",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "视频",
|
||||
"video_models": "视频模型"
|
||||
},
|
||||
|
||||
@@ -7366,19 +7366,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveSVG": {
|
||||
"description": "将 SVG 文件保存到磁盘。",
|
||||
"display_name": "保存 SVG",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
"name": "文件名前缀",
|
||||
"tooltip": "要保存文件的前缀。可以包含格式化信息,如 %date:yyyy-MM-dd% 或 %Empty Latent Image.width%,以包含来自节点的数值。"
|
||||
},
|
||||
"svg": {
|
||||
"name": "svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveVideo": {
|
||||
"description": "将输入图像保存到您的 ComfyUI 输出目录。",
|
||||
"display_name": "保存视频",
|
||||
@@ -8657,4 +8644,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,6 @@
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "校验工作流"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "启用 Vue 节点渲染",
|
||||
"tooltip": "将节点渲染为 Vue 组件,而不是画布元素。实验性功能。"
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "启用Vue小部件",
|
||||
"tooltip": "在Vue节点中将小部件渲染为Vue组件。"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "组件控制模式",
|
||||
"options": {
|
||||
@@ -396,10 +388,6 @@
|
||||
"Topbar (2nd-row)": "顶部栏 (第二行)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低质量渲染缩放阈值",
|
||||
"tooltip": "在缩小时渲染低质量形状"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "画布允许渲染的最大帧数。限制GPU使用以换取流畅度。如果为0,则使用屏幕刷新率。默认值:0"
|
||||
@@ -421,4 +409,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "始终吸附到网格"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
const ASSETS_ENDPOINT = '/assets'
|
||||
const MODELS_TAG = 'models'
|
||||
const MISSING_TAG = 'missing'
|
||||
const EXPERIMENTAL_WARNING = `EXPERIMENTAL: If you are seeing this please make sure "Comfy.Assets.UseAssetAPI" is set to "false" in your ComfyUI Settings.\n`
|
||||
|
||||
/**
|
||||
* Input names that are eligible for asset browser
|
||||
@@ -26,7 +27,9 @@ function validateAssetResponse(data: unknown): AssetResponse {
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
throw new Error(`Invalid asset response against zod schema:\n${error}`)
|
||||
throw new Error(
|
||||
`${EXPERIMENTAL_WARNING}Invalid asset response against zod schema:\n${error}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +47,7 @@ function createAssetService() {
|
||||
const res = await api.fetchApi(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Unable to load ${context}: Server returned ${res.status}. Please try again.`
|
||||
`${EXPERIMENTAL_WARNING}Unable to load ${context}: Server returned ${res.status}. Please try again.`
|
||||
)
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
@@ -1051,7 +1051,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Assets.UseAssetAPI',
|
||||
name: 'Use Asset API for model library',
|
||||
type: 'boolean',
|
||||
type: 'hidden',
|
||||
tooltip: 'Use new Asset API for model browsing',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
|
||||
@@ -17,7 +17,7 @@ import { downloadBlob } from '@/scripts/utils'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt, generateUUID } from '@/utils/formatUtil'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
export const useWorkflowService = () => {
|
||||
const settingStore = useSettingStore()
|
||||
@@ -112,13 +112,6 @@ export const useWorkflowService = () => {
|
||||
await renameWorkflow(workflow, newPath)
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
} else {
|
||||
// Generate new id when saving existing workflow as a new file
|
||||
const id = generateUUID()
|
||||
const state = JSON.parse(
|
||||
JSON.stringify(workflow.activeState)
|
||||
) as ComfyWorkflowJSON
|
||||
state.id = id
|
||||
|
||||
const tempWorkflow = workflowStore.saveAs(workflow, newPath)
|
||||
await openWorkflow(tempWorkflow)
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { generateUUID, getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -320,12 +320,19 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
existingWorkflow: ComfyWorkflow,
|
||||
path: string
|
||||
): ComfyWorkflow => {
|
||||
// Generate new id when saving existing workflow as a new file
|
||||
const id = generateUUID()
|
||||
const state = JSON.parse(
|
||||
JSON.stringify(existingWorkflow.activeState)
|
||||
) as ComfyWorkflowJSON
|
||||
state.id = id
|
||||
|
||||
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
|
||||
path,
|
||||
modified: Date.now(),
|
||||
size: -1
|
||||
})
|
||||
workflow.originalContent = workflow.content = existingWorkflow.content
|
||||
workflow.originalContent = workflow.content = JSON.stringify(state)
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ const router = createRouter({
|
||||
name: 'DesktopUpdateView',
|
||||
component: () => import('@/views/DesktopUpdateView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
},
|
||||
{
|
||||
path: 'desktop-dialog/:dialogId',
|
||||
name: 'DesktopDialogView',
|
||||
component: () => import('@/views/DesktopDialogView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,16 +4,3 @@ export enum ValidationState {
|
||||
VALID = 'VALID',
|
||||
INVALID = 'INVALID'
|
||||
}
|
||||
|
||||
export const mergeValidationStates = (states: ValidationState[]) => {
|
||||
if (states.some((state) => state === ValidationState.INVALID)) {
|
||||
return ValidationState.INVALID
|
||||
}
|
||||
if (states.some((state) => state === ValidationState.LOADING)) {
|
||||
return ValidationState.LOADING
|
||||
}
|
||||
if (states.every((state) => state === ValidationState.VALID)) {
|
||||
return ValidationState.VALID
|
||||
}
|
||||
return ValidationState.IDLE
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
||||
<StartupDisplay :title="$t('desktopStart.initialising')" />
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||
|
||||
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||
</script>
|
||||
|
||||
423
src/views/InstallView.stories.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { nextTick, provide } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import InstallView from './InstallView.vue'
|
||||
|
||||
// Create a mock router for stories
|
||||
const createMockRouter = () =>
|
||||
createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/server-start',
|
||||
component: { template: '<div>Server Start</div>' }
|
||||
},
|
||||
{
|
||||
path: '/manual-configuration',
|
||||
component: { template: '<div>Manual Configuration</div>' }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const meta: Meta<typeof InstallView> = {
|
||||
title: 'Desktop/Views/InstallView',
|
||||
component: InstallView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
(story) => {
|
||||
// Create router for this story
|
||||
const router = createMockRouter()
|
||||
|
||||
// Mock electron API
|
||||
;(window as any).electronAPI = {
|
||||
getPlatform: () => 'darwin',
|
||||
Config: {
|
||||
getDetectedGpu: () => Promise.resolve('mps')
|
||||
},
|
||||
Events: {
|
||||
trackEvent: (eventName: string, data?: any) => {
|
||||
console.log('Track event:', eventName, data)
|
||||
}
|
||||
},
|
||||
installComfyUI: (options: any) => {
|
||||
console.log('Install ComfyUI with options:', options)
|
||||
},
|
||||
changeTheme: (theme: any) => {
|
||||
console.log('Change theme:', theme)
|
||||
},
|
||||
getSystemPaths: () =>
|
||||
Promise.resolve({
|
||||
defaultInstallPath: '/Users/username/ComfyUI'
|
||||
}),
|
||||
validateInstallPath: () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false
|
||||
}),
|
||||
validateComfyUISource: () =>
|
||||
Promise.resolve({
|
||||
isValid: true
|
||||
}),
|
||||
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||
}
|
||||
|
||||
return {
|
||||
setup() {
|
||||
// Provide router for all child components
|
||||
provide('router', router)
|
||||
return {
|
||||
story
|
||||
}
|
||||
},
|
||||
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default story - start at GPU selection
|
||||
export const GpuSelection: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
// The component will automatically start at step 1
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the install location step
|
||||
export const InstallLocation: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the migration step (currently empty)
|
||||
export const MigrationStep: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the desktop settings configuration
|
||||
export const DesktopSettings: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 4
|
||||
const buttons3 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn3) {
|
||||
nextBtn3.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with Windows platform (no Apple Metal option)
|
||||
export const WindowsPlatform: Story = {
|
||||
render: () => {
|
||||
// Override the platform to Windows
|
||||
;(window as any).electronAPI.getPlatform = () => 'win32'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('nvidia')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with macOS platform (Apple Metal option)
|
||||
export const MacOSPlatform: Story = {
|
||||
name: 'macOS Platform',
|
||||
render: () => {
|
||||
// Override the platform to macOS
|
||||
;(window as any).electronAPI.getPlatform = () => 'darwin'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('mps')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with CPU selected
|
||||
export const CpuSelected: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the CPU hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// CPU is the button with "CPU" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('CPU')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with manual install selected
|
||||
export const ManualInstall: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the Manual Install hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// Manual Install is the button with "Manual Install" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('Manual Install')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with error state (invalid install path)
|
||||
export const ErrorState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return an error
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: false,
|
||||
exists: false,
|
||||
canWrite: false,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false,
|
||||
error: 'Story mock: Example error state'
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where error will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with warning state (non-default drive)
|
||||
export const WarningState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return a warning about non-default drive
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 500_000_000_000,
|
||||
requiredSpace: 10_000_000_000,
|
||||
isNonDefaultDrive: true
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where warning will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,54 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<!-- h-full to make sure the stepper does not layout shift between steps
|
||||
as for each step the stepper height is different. Inherit the center element
|
||||
placement from BaseViewTemplate would cause layout shift. -->
|
||||
<Stepper
|
||||
class="h-full p-8 2xl:p-16"
|
||||
value="0"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<StepList class="select-none">
|
||||
<Step value="0">
|
||||
{{ $t('install.gpu') }}
|
||||
</Step>
|
||||
<Step value="1" :disabled="noGpu">
|
||||
{{ $t('install.installLocation') }}
|
||||
</Step>
|
||||
<Step value="2" :disabled="noGpu || hasError || highestStep < 1">
|
||||
{{ $t('install.migration') }}
|
||||
</Step>
|
||||
<Step value="3" :disabled="noGpu || hasError || highestStep < 2">
|
||||
{{ $t('install.desktopSettings') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels>
|
||||
<StepPanel v-slot="{ activateCallback }" value="0">
|
||||
<GpuPicker v-model:device="device" />
|
||||
<div class="flex pt-6 justify-end">
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="typeof device !== 'string'"
|
||||
@click="activateCallback('1')"
|
||||
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<Stepper
|
||||
v-model:value="currentStep"
|
||||
class="flex flex-col h-full"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<!-- Main content area that grows to fill available space -->
|
||||
<StepPanels
|
||||
class="flex-1 overflow-auto"
|
||||
:style="{ scrollbarGutter: 'stable' }"
|
||||
>
|
||||
<StepPanel value="1" class="flex">
|
||||
<GpuPicker v-model:device="device" />
|
||||
</StepPanel>
|
||||
<StepPanel value="2">
|
||||
<InstallLocationPicker
|
||||
v-model:install-path="installPath"
|
||||
v-model:path-error="pathError"
|
||||
v-model:migration-source-path="migrationSourcePath"
|
||||
v-model:migration-item-ids="migrationItemIds"
|
||||
v-model:python-mirror="pythonMirror"
|
||||
v-model:pypi-mirror="pypiMirror"
|
||||
v-model:torch-mirror="torchMirror"
|
||||
:device="device"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="1">
|
||||
<InstallLocationPicker
|
||||
v-model:install-path="installPath"
|
||||
v-model:path-error="pathError"
|
||||
/>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('0')"
|
||||
</StepPanel>
|
||||
<StepPanel value="3">
|
||||
<DesktopSettingsConfiguration
|
||||
v-model:auto-update="autoUpdate"
|
||||
v-model:allow-metrics="allowMetrics"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="pathError !== ''"
|
||||
@click="activateCallback('2')"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="2">
|
||||
<MigrationPicker
|
||||
v-model:source-path="migrationSourcePath"
|
||||
v-model:migration-item-ids="migrationItemIds"
|
||||
/>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('1')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
@click="activateCallback('3')"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="3">
|
||||
<DesktopSettingsConfiguration
|
||||
v-model:auto-update="autoUpdate"
|
||||
v-model:allow-metrics="allowMetrics"
|
||||
/>
|
||||
<MirrorsConfiguration
|
||||
v-model:python-mirror="pythonMirror"
|
||||
v-model:pypi-mirror="pypiMirror"
|
||||
v-model:torch-mirror="torchMirror"
|
||||
:device="device"
|
||||
class="mt-6"
|
||||
/>
|
||||
<div class="flex mt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('2')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.install')"
|
||||
icon="pi pi-check"
|
||||
icon-pos="right"
|
||||
:disabled="hasError"
|
||||
@click="install()"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
|
||||
<!-- Install footer with navigation -->
|
||||
<InstallFooter
|
||||
class="w-full max-w-2xl my-6 mx-auto"
|
||||
:current-step
|
||||
:can-proceed
|
||||
:disable-location-step="noGpu"
|
||||
:disable-migration-step="noGpu || hasError || highestStep < 2"
|
||||
:disable-settings-step="noGpu || hasError || highestStep < 3"
|
||||
@previous="goToPreviousStep"
|
||||
@next="goToNextStep"
|
||||
@install="install"
|
||||
/>
|
||||
</Stepper>
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
@@ -114,9 +57,6 @@ import type {
|
||||
InstallOptions,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Button from 'primevue/button'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
@@ -125,9 +65,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
||||
import GpuPicker from '@/components/install/GpuPicker.vue'
|
||||
import InstallFooter from '@/components/install/InstallFooter.vue'
|
||||
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorsConfiguration from '@/components/install/MirrorsConfiguration.vue'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
@@ -145,6 +84,9 @@ const pythonMirror = ref('')
|
||||
const pypiMirror = ref('')
|
||||
const torchMirror = ref('')
|
||||
|
||||
/** Current step in the stepper */
|
||||
const currentStep = ref('1')
|
||||
|
||||
/** Forces each install step to be visited at least once. */
|
||||
const highestStep = ref(0)
|
||||
|
||||
@@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => {
|
||||
const hasError = computed(() => pathError.value !== '')
|
||||
const noGpu = computed(() => typeof device.value !== 'string')
|
||||
|
||||
// Computed property to determine if user can proceed to next step
|
||||
const regex = /^Insufficient space - minimum free space: \d+ GB$/
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
case '1':
|
||||
return typeof device.value === 'string'
|
||||
case '2':
|
||||
return pathError.value === '' || regex.test(pathError.value)
|
||||
case '3':
|
||||
return !hasError.value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Navigation methods
|
||||
const goToNextStep = () => {
|
||||
const nextStep = (parseInt(currentStep.value) + 1).toString()
|
||||
currentStep.value = nextStep
|
||||
setHighestStep(nextStep)
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: nextStep
|
||||
})
|
||||
}
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
const prevStep = (parseInt(currentStep.value) - 1).toString()
|
||||
currentStep.value = prevStep
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: prevStep
|
||||
})
|
||||
}
|
||||
|
||||
const electron = electronAPI()
|
||||
const router = useRouter()
|
||||
const install = async () => {
|
||||
@@ -195,7 +171,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: '0',
|
||||
step: currentStep.value,
|
||||
gpu: detectedGpu
|
||||
})
|
||||
})
|
||||
@@ -205,6 +181,30 @@ onMounted(async () => {
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
:deep(.p-steppanel) {
|
||||
@apply mt-8 flex justify-center bg-transparent;
|
||||
}
|
||||
|
||||
/* Remove default padding/margin from StepPanels to make scrollbar flush */
|
||||
:deep(.p-steppanels) {
|
||||
@apply p-0 m-0;
|
||||
}
|
||||
|
||||
/* Ensure StepPanel content container has no top/bottom padding */
|
||||
:deep(.p-steppanel-content) {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
|
||||
:deep(.p-steppanels::-webkit-scrollbar) {
|
||||
@apply w-4;
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-track) {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
|
||||
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,75 +1,193 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="flex-col">
|
||||
<div class="flex flex-col w-full h-full items-center">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-wrench"
|
||||
:label="t('serverStart.troubleshoot')"
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
<BaseViewTemplate dark>
|
||||
<div class="relative min-h-screen">
|
||||
<!-- Terminal Background Layer (always visible during loading) -->
|
||||
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
||||
<div class="h-full w-full">
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Semi-transparent overlay -->
|
||||
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
||||
|
||||
<!-- Smooth radial gradient overlay -->
|
||||
<div
|
||||
v-if="!isError"
|
||||
class="fixed inset-0 z-8"
|
||||
style="
|
||||
background: radial-gradient(
|
||||
ellipse 800px 600px at center,
|
||||
rgba(23, 23, 23, 0.95) 0%,
|
||||
rgba(23, 23, 23, 0.93) 10%,
|
||||
rgba(23, 23, 23, 0.9) 20%,
|
||||
rgba(23, 23, 23, 0.85) 30%,
|
||||
rgba(23, 23, 23, 0.75) 40%,
|
||||
rgba(23, 23, 23, 0.6) 50%,
|
||||
rgba(23, 23, 23, 0.4) 60%,
|
||||
rgba(23, 23, 23, 0.2) 70%,
|
||||
rgba(23, 23, 23, 0.1) 80%,
|
||||
rgba(23, 23, 23, 0.05) 90%,
|
||||
transparent 100%
|
||||
);
|
||||
"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<!-- Main startup display using StartupDisplay component -->
|
||||
<StartupDisplay
|
||||
:title="displayTitle"
|
||||
:status-text="displayStatusText"
|
||||
:progress-percentage="installStageProgress"
|
||||
:hide-progress="isError"
|
||||
/>
|
||||
|
||||
<!-- Error Section (positioned at bottom) -->
|
||||
<div
|
||||
v-if="isError"
|
||||
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
:label="$t('serverStart.reportIssue')"
|
||||
severity="secondary"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
:label="$t('serverStart.openLogs')"
|
||||
severity="secondary"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-wrench"
|
||||
:label="$t('serverStart.troubleshoot')"
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||
<div
|
||||
v-if="terminalVisible && isError"
|
||||
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
|
||||
>
|
||||
<div
|
||||
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
|
||||
>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
</div>
|
||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import {
|
||||
InstallStage,
|
||||
type InstallStageInfo,
|
||||
type InstallStageName,
|
||||
ProgressStatus
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
import { Ref, onMounted, ref } from 'vue'
|
||||
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const electron = electronAPI()
|
||||
const { t } = useI18n()
|
||||
const electron = electronAPI()
|
||||
|
||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||
const electronVersion = ref<string>('')
|
||||
const terminalVisible = ref(false)
|
||||
|
||||
const installStage = ref<InstallStageName | null>(null)
|
||||
const installStageMessage = ref<string>('')
|
||||
const installStageProgress = ref<number | undefined>(undefined)
|
||||
|
||||
let xterm: Terminal | undefined
|
||||
|
||||
const terminalVisible = ref(true)
|
||||
/**
|
||||
* Handles installation stage updates from the desktop
|
||||
*/
|
||||
const updateInstallStage = (stageInfo: InstallStageInfo) => {
|
||||
console.warn('[InstallStage.onUpdate] Received:', {
|
||||
stage: stageInfo.stage,
|
||||
progress: stageInfo.progress,
|
||||
message: stageInfo.message,
|
||||
error: stageInfo.error,
|
||||
timestamp: stageInfo.timestamp,
|
||||
fullInfo: stageInfo
|
||||
})
|
||||
|
||||
installStage.value = stageInfo.stage
|
||||
installStageMessage.value = stageInfo.message || ''
|
||||
installStageProgress.value = stageInfo.progress
|
||||
}
|
||||
|
||||
const currentStatusLabel = computed(() => {
|
||||
// Use the message from the Electron API if available
|
||||
if (installStageMessage.value) {
|
||||
return installStageMessage.value
|
||||
}
|
||||
return t(`serverStart.process.${status.value}`)
|
||||
})
|
||||
|
||||
const isError = computed(
|
||||
() =>
|
||||
status.value === ProgressStatus.ERROR ||
|
||||
installStage.value === InstallStage.ERROR
|
||||
)
|
||||
|
||||
const isInstallationStage = computed(() => {
|
||||
const installationStages: InstallStageName[] = [
|
||||
InstallStage.WELCOME_SCREEN,
|
||||
InstallStage.INSTALL_OPTIONS_SELECTION,
|
||||
InstallStage.CREATING_DIRECTORIES,
|
||||
InstallStage.INITIALIZING_CONFIG,
|
||||
InstallStage.PYTHON_ENVIRONMENT_SETUP,
|
||||
InstallStage.INSTALLING_REQUIREMENTS,
|
||||
InstallStage.INSTALLING_PYTORCH,
|
||||
InstallStage.INSTALLING_COMFYUI_REQUIREMENTS,
|
||||
InstallStage.INSTALLING_MANAGER_REQUIREMENTS,
|
||||
InstallStage.MIGRATING_CUSTOM_NODES
|
||||
]
|
||||
return (
|
||||
installStage.value !== null &&
|
||||
installationStages.includes(installStage.value)
|
||||
)
|
||||
})
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
if (isError.value) {
|
||||
return t('serverStart.errorMessage')
|
||||
}
|
||||
if (isInstallationStage.value) {
|
||||
return t('serverStart.installation.title')
|
||||
}
|
||||
return t('serverStart.title')
|
||||
})
|
||||
|
||||
const displayStatusText = computed(() => {
|
||||
if (isError.value && electronVersion.value) {
|
||||
return `v${electronVersion.value}`
|
||||
}
|
||||
return currentStatusLabel.value
|
||||
})
|
||||
|
||||
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||
status.value = newStatus
|
||||
|
||||
// Make critical error screen more obvious.
|
||||
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
|
||||
else xterm?.clear()
|
||||
}
|
||||
|
||||
const terminalCreated = (
|
||||
@@ -94,9 +212,30 @@ const reportIssue = () => {
|
||||
}
|
||||
const openLogs = () => electron.openLogsFolder()
|
||||
|
||||
let cleanupInstallStageListener: (() => void) | undefined
|
||||
|
||||
onMounted(async () => {
|
||||
electron.sendReady()
|
||||
electron.onProgressUpdate(updateProgress)
|
||||
cleanupInstallStageListener =
|
||||
electron.InstallStage.onUpdate(updateInstallStage)
|
||||
|
||||
const stageInfo = await electron.InstallStage.getCurrent()
|
||||
updateInstallStage(stageInfo)
|
||||
electronVersion.value = await electron.getElectronVersion()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
xterm?.dispose()
|
||||
cleanupInstallStageListener?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
/* Hide the xterm scrollbar completely */
|
||||
:deep(.p-terminal) .xterm-viewport {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<h1 class="animated-gradient-text text-glow select-none">
|
||||
{{ $t('welcome.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Get Started Button -->
|
||||
<Button
|
||||
:label="$t('welcome.getStarted')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
size="large"
|
||||
rounded
|
||||
class="p-4 text-lg fade-in-up"
|
||||
@click="navigateTo('/install')"
|
||||
/>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="grid grid-rows-2 gap-8">
|
||||
<!-- Top container: Logo -->
|
||||
<div class="flex items-end justify-center">
|
||||
<img
|
||||
src="/assets/images/comfy-brand-mark.svg"
|
||||
:alt="$t('g.logoAlt')"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
<!-- Bottom container: Title and button -->
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<Button
|
||||
:label="$t('welcome.getStarted')"
|
||||
class="px-8 mt-4 bg-brand-yellow hover:bg-brand-yellow/90 border-0 rounded-lg transition-colors"
|
||||
:pt="{
|
||||
label: { class: 'font-inter text-neutral-900 font-black' }
|
||||
}"
|
||||
@click="navigateTo('/install')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
@@ -31,49 +37,3 @@ const navigateTo = async (path: string) => {
|
||||
await router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.animated-gradient-text {
|
||||
@apply font-bold;
|
||||
font-size: clamp(2rem, 8vw, 4rem);
|
||||
background: linear-gradient(to right, #12c2e9, #c471ed, #f64f59, #12c2e9);
|
||||
background-size: 300% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient 8s linear infinite;
|
||||
}
|
||||
|
||||
.text-glow {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% center;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 300% center;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 1.5s ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -269,7 +269,115 @@ describe('useNodePricing', () => {
|
||||
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
|
||||
})
|
||||
})
|
||||
// ============================== OpenAIVideoSora2 ==============================
|
||||
describe('dynamic pricing - OpenAIVideoSora2', () => {
|
||||
it('should require model, duration & size when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [])
|
||||
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
|
||||
})
|
||||
|
||||
it('should require duration when duration is invalid or zero', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const nodeNaN = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 'oops' },
|
||||
{ name: 'size', value: '720x1280' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set model, duration & size')
|
||||
|
||||
const nodeZero = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 0 },
|
||||
{ name: 'size', value: '720x1280' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(nodeZero)).toBe('Set model, duration & size')
|
||||
})
|
||||
|
||||
it('should require size when size is missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 8 }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
|
||||
})
|
||||
|
||||
it('should compute pricing for sora-2-pro with 1024x1792', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 8 },
|
||||
{ name: 'size', value: '1024x1792' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
|
||||
})
|
||||
|
||||
it('should compute pricing for sora-2-pro with 720x1280', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 12 },
|
||||
{ name: 'size', value: '720x1280' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
|
||||
})
|
||||
|
||||
it('should reject unsupported size for sora-2-pro', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration', value: 8 },
|
||||
{ name: 'size', value: '640x640' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(
|
||||
'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should compute pricing for sora-2 (720x1280 only)', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2' },
|
||||
{ name: 'duration', value: 10 },
|
||||
{ name: 'size', value: '720x1280' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
|
||||
})
|
||||
|
||||
it('should reject non-720 sizes for sora-2', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2' },
|
||||
{ name: 'duration', value: 8 },
|
||||
{ name: 'size', value: '1024x1792' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(
|
||||
'sora-2 supports only 720x1280 or 1280x720'
|
||||
)
|
||||
})
|
||||
it('should accept duration_s alias for duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'sora-2-pro' },
|
||||
{ name: 'duration_s', value: 4 },
|
||||
{ name: 'size', value: '1792x1024' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
|
||||
})
|
||||
|
||||
it('should be case-insensitive for model and size', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIVideoSora2', [
|
||||
{ name: 'model', value: 'SoRa-2-PrO' },
|
||||
{ name: 'duration', value: 12 },
|
||||
{ name: 'size', value: '1280x720' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
|
||||
})
|
||||
})
|
||||
|
||||
// ============================== MinimaxHailuoVideoNode ==============================
|
||||
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
|
||||
it('should return $0.28 for 6s duration and 768P resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
@@ -1894,4 +2002,159 @@ describe('useNodePricing', () => {
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - WanTextToVideoApi', () => {
|
||||
it('should return $1.50 for 10s at 1080p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.50/Run') // 0.15 * 10
|
||||
})
|
||||
|
||||
it('should return $0.50 for 5s at 720p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: 5 },
|
||||
{ name: 'size', value: '720p: 16:9 (1280x720)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.50/Run') // 0.10 * 5
|
||||
})
|
||||
|
||||
it('should return $0.15 for 3s at 480p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '3' },
|
||||
{ name: 'size', value: '480p: 1:1 (624x624)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.15/Run') // 0.05 * 3
|
||||
})
|
||||
|
||||
it('should fall back when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const missingBoth = createMockNode('WanTextToVideoApi', [])
|
||||
const missingSize = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '5' }
|
||||
])
|
||||
const missingDuration = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'size', value: '1080p' }
|
||||
])
|
||||
|
||||
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on invalid duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: 'invalid' },
|
||||
{ name: 'size', value: '1080p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on unknown resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'size', value: '2K' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - WanImageToVideoApi', () => {
|
||||
it('should return $0.80 for 8s at 720p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: 8 },
|
||||
{ name: 'resolution', value: '720p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.80/Run') // 0.10 * 8
|
||||
})
|
||||
|
||||
it('should return $0.60 for 12s at 480P', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '12' },
|
||||
{ name: 'resolution', value: '480P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.60/Run') // 0.05 * 12
|
||||
})
|
||||
|
||||
it('should return $1.50 for 10s at 1080p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'resolution', value: '1080p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.50/Run') // 0.15 * 10
|
||||
})
|
||||
|
||||
it('should handle "5s" string duration at 1080P', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '5s' },
|
||||
{ name: 'resolution', value: '1080P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.75/Run') // 0.15 * 5
|
||||
})
|
||||
|
||||
it('should fall back when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const missingBoth = createMockNode('WanImageToVideoApi', [])
|
||||
const missingRes = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '5' }
|
||||
])
|
||||
const missingDuration = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'resolution', value: '1080p' }
|
||||
])
|
||||
|
||||
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on invalid duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: 'invalid' },
|
||||
{ name: 'resolution', value: '720p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on unknown resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'resolution', value: 'weird-res' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,10 +56,10 @@ describe('useManagerState', () => {
|
||||
})
|
||||
|
||||
describe('managerUIState property', () => {
|
||||
it('should return DISABLED state when --disable-manager is present', () => {
|
||||
it('should return DISABLED state when --enable-manager is NOT present', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--disable-manager'] }
|
||||
system: { argv: ['python', 'main.py'] } // No --enable-manager flag
|
||||
}),
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
@@ -76,7 +76,14 @@ describe('useManagerState', () => {
|
||||
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
|
||||
system: {
|
||||
argv: [
|
||||
'python',
|
||||
'main.py',
|
||||
'--enable-manager',
|
||||
'--enable-manager-legacy-ui'
|
||||
]
|
||||
} // Both flags needed
|
||||
}),
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
@@ -92,7 +99,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('should return NEW_UI state when client and server both support v4', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
@@ -114,7 +123,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
@@ -136,7 +147,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('should return LEGACY_UI state when legacy manager extension exists', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
@@ -155,7 +168,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('should return NEW_UI state when server feature flags are undefined', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
@@ -175,7 +190,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('should return LEGACY_UI state when server does not support v4', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
@@ -212,14 +229,17 @@ describe('useManagerState', () => {
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
|
||||
// When systemStats is null, we can't check for --enable-manager flag, so manager is disabled
|
||||
expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('helper properties', () => {
|
||||
it('isManagerEnabled should return true when state is not DISABLED', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
@@ -237,7 +257,7 @@ describe('useManagerState', () => {
|
||||
it('isManagerEnabled should return false when state is DISABLED', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--disable-manager'] }
|
||||
system: { argv: ['python', 'main.py'] } // No --enable-manager flag means disabled
|
||||
}),
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
@@ -252,7 +272,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('isNewManagerUI should return true when state is NEW_UI', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
@@ -270,7 +292,14 @@ describe('useManagerState', () => {
|
||||
it('isLegacyManagerUI should return true when state is LEGACY_UI', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
|
||||
system: {
|
||||
argv: [
|
||||
'python',
|
||||
'main.py',
|
||||
'--enable-manager',
|
||||
'--enable-manager-legacy-ui'
|
||||
]
|
||||
} // Both flags needed
|
||||
}),
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
@@ -285,7 +314,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('shouldShowInstallButton should return true only for NEW_UI', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
@@ -302,7 +333,9 @@ describe('useManagerState', () => {
|
||||
|
||||
it('shouldShowManagerButtons should return true when not DISABLED', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
|
||||
systemStats: ref({
|
||||
system: { argv: ['python', 'main.py', '--enable-manager'] }
|
||||
}), // Need --enable-manager
|
||||
isInitialized: ref(true)
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
|
||||