Compare commits
22 Commits
graphMutat
...
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',
|
||||
@@ -75,8 +84,6 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-import-type-side-effects': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
|
||||
@@ -27,8 +27,7 @@ const config: KnipConfig = {
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Staged for for use with subgraph widget promotion
|
||||
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts',
|
||||
'src/core/graph/operations/types.ts'
|
||||
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { CSSProperties, computed, watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -26,8 +29,7 @@
|
||||
import { useElementHover, useEventListener } from '@vueuse/core'
|
||||
import type { IDisposable } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
@@ -98,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>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDisposable } from '@xterm/xterm'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { IDisposable } from '@xterm/xterm'
|
||||
import { Ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
import { until } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Ref, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import type { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import type { MenuState } from 'primevue/menu'
|
||||
import Menu from 'primevue/menu'
|
||||
import Menu, { MenuState } from 'primevue/menu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
|
||||
import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
|
||||
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
extension: VueExtension | CustomExtension
|
||||
|
||||
@@ -44,7 +44,7 @@ import FormRadioGroup from '@/components/common/FormRadioGroup.vue'
|
||||
import InputKnob from '@/components/common/InputKnob.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import type { FormItem } from '@/platform/settings/types'
|
||||
import { FormItem } from '@/platform/settings/types'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
import type { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
import { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
|
||||
const {
|
||||
disabled,
|
||||
|
||||
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>
|
||||
@@ -9,8 +9,10 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { InjectKeyHandleEditLabelFunction } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
RenderedTreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
@@ -148,7 +148,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -160,7 +160,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { initialTab } = defineProps<{
|
||||
initialTab?: ManagerTab
|
||||
|
||||
@@ -169,7 +169,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import type {
|
||||
import {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Message from 'primevue/message'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
@@ -93,8 +93,8 @@ import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
|
||||
@@ -31,11 +31,13 @@ import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ButtonSize } from '@/types/buttonTypes'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import {
|
||||
type ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<script setup lang="ts">
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ButtonSize } from '@/types/buttonTypes'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ import { useImportFailedDetection } from '@/composables/useImportFailedDetection
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import PackUninstallButton from '@/components/dialog/content/manager/button/Pack
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import { computed, inject, ref, watchEffect } from 'vue'
|
||||
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
|
||||
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
|
||||
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import DescriptionTabPanel from './DescriptionTabPanel.vue'
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import InfoTextSection, {
|
||||
type TextSection
|
||||
} from '@/components/dialog/content/manager/infoPanel/InfoTextSection.vue'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -34,7 +34,7 @@ import { computed, ref, shallowRef, useId } from 'vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { registryToFrontendV2NodeDef } from '@/utils/mapperUtil'
|
||||
|
||||
type ListComfyNodesResponse =
|
||||
|
||||
@@ -29,8 +29,8 @@ import { computed } from 'vue'
|
||||
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { t } from '@/i18n'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { nodePack, conflictResult } = defineProps<{
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const {
|
||||
nodePacks,
|
||||
|
||||
@@ -62,8 +62,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stubTrue } from 'es-toolkit/compat'
|
||||
import type { AutoCompleteOptionSelectEvent } from 'primevue/autocomplete'
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
import AutoComplete, {
|
||||
AutoCompleteOptionSelectEvent
|
||||
} from 'primevue/autocomplete'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -76,7 +77,7 @@ import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type {
|
||||
QuerySuggestion,
|
||||
SearchMode,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
@@ -96,8 +96,8 @@ import Message from 'primevue/message'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import {
|
||||
AuditLog,
|
||||
EventType,
|
||||
useCustomerEventsService
|
||||
} from '@/services/customerEventsService'
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { KeyComboImpl } from '@/stores/keybindingStore'
|
||||
import { KeyComboImpl } from '@/stores/keybindingStore'
|
||||
|
||||
const { keyCombo, isModified = false } = defineProps<{
|
||||
keyCombo: KeyComboImpl
|
||||
|
||||
@@ -79,8 +79,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
@@ -71,8 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form, FormField } from '@primevue/forms'
|
||||
import { Form, FormField, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
|
||||
@@ -135,8 +135,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { InputNumberInputEvent } from 'primevue'
|
||||
import { Button, InputNumber } from 'primevue'
|
||||
import { Button, InputNumber, InputNumberInputEvent } from 'primevue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import type { Raw } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Raw, computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
MenuOption,
|
||||
SubMenuOption
|
||||
import {
|
||||
type MenuOption,
|
||||
type SubMenuOption
|
||||
} from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useNodeCustomization } from '@/composables/graph/useNodeCustomization'
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
|
||||
import ResponseBlurb from '@/components/graph/widgets/chatHistory/ResponseBlurb.vue'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const { widget, history } = defineProps<{
|
||||
|
||||
@@ -19,15 +19,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useEventListener } from '@vueuse/core'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { CSSProperties, computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { useDomClipping } from '@/composables/element/useDomClipping'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { isComponentWidget, isDOMWidget } from '@/scripts/domWidget'
|
||||
import type { DomWidgetState } from '@/stores/domWidgetStore'
|
||||
import { DomWidgetState } from '@/stores/domWidgetStore'
|
||||
|
||||
const { widgetState } = defineProps<{
|
||||
widgetState: DomWidgetState
|
||||
@@ -125,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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
|
||||
import { type ReleaseNote } from '@/platform/updates/common/releaseService'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MultiSelectProps } from 'primevue/multiselect'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelect from './MultiSelect.vue'
|
||||
import type { SelectOption } from './types'
|
||||
import { type SelectOption } from './types'
|
||||
|
||||
// Combine our component props with PrimeVue MultiSelect props
|
||||
interface ExtendedProps extends Partial<MultiSelectProps> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MultiSelectProps } from 'primevue/multiselect'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelect from './MultiSelect.vue'
|
||||
import type { SelectOption } from './types'
|
||||
import { type SelectOption } from './types'
|
||||
|
||||
// Combine our component props with PrimeVue MultiSelect props
|
||||
// Since we use v-bind="$attrs", all PrimeVue props are available
|
||||
|
||||
@@ -106,8 +106,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { MultiSelectPassThroughMethodOptions } from 'primevue/multiselect'
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
import MultiSelect, {
|
||||
MultiSelectPassThroughMethodOptions
|
||||
} from 'primevue/multiselect'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -116,7 +117,7 @@ import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TextButton from '../button/TextButton.vue'
|
||||
import type { SelectOption } from './types'
|
||||
import { type SelectOption } from './types'
|
||||
|
||||
type Option = SelectOption
|
||||
|
||||
|
||||
@@ -58,14 +58,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectPassThroughMethodOptions } from 'primevue/select'
|
||||
import Select from 'primevue/select'
|
||||
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { SelectOption } from './types'
|
||||
import { type SelectOption } from './types'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
|
||||
@@ -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 type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
|
||||
import Divider from 'primevue/divider'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { ModelRef } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import type { UVMirror } from '@/constants/uvMirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } 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 type { UVMirror } from '@/constants/uvMirrors'
|
||||
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
|
||||
})
|
||||
|
||||
@@ -95,7 +95,7 @@ import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
import {
|
||||
CameraType,
|
||||
Load3DNodeType,
|
||||
MaterialMode,
|
||||
|
||||
@@ -93,7 +93,7 @@ import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
|
||||
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
import {
|
||||
AnimationItem,
|
||||
CameraType,
|
||||
Load3DAnimationNodeType,
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import type {
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const props = defineProps<{
|
||||
node: any
|
||||
|
||||
@@ -99,7 +99,7 @@ import ExportControls from '@/components/load3d/controls/ExportControls.vue'
|
||||
import LightControls from '@/components/load3d/controls/LightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
|
||||
import type {
|
||||
import {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import type {
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ import LightControls from '@/components/load3d/controls/viewer/ViewerLightContro
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -99,10 +99,7 @@ import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type {
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { MaterialMode, UpDirection } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -20,7 +20,7 @@ import Button from 'primevue/button'
|
||||
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import Select from 'primevue/select'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const cameras = [
|
||||
|
||||
@@ -26,10 +26,7 @@
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type {
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { MaterialMode, UpDirection } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
|
||||
@@ -47,7 +47,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import type { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
import { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
import TaskListStatusIcon from './TaskListStatusIcon.vue'
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import type { MaybeRef } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { MaybeRef, computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
|
||||
@@ -10,10 +10,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import Drawer from 'primevue/drawer'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { Ref, onMounted } from 'vue'
|
||||
|
||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
|
||||
@@ -88,8 +88,11 @@ import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
|
||||
@@ -38,19 +38,21 @@ import { storeToRefs } from 'pinia'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
|
||||
|
||||
import type { Point } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { Point } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
|
||||
|
||||
@@ -27,9 +27,8 @@ import Select from 'primevue/select'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import type { FuseFilter, FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { FuseFilter, FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
const filters = computed(() => nodeDefStore.nodeSearchService.nodeFilters)
|
||||
const selectedFilter = ref<FuseFilter<ComfyNodeDefImpl, string>>()
|
||||
|
||||