mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-02 13:17:48 +00:00
Compare commits
21 Commits
bl-fix-mon
...
fix/sentry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c844711f6d | ||
|
|
28cb3bbdb5 | ||
|
|
d59885839a | ||
|
|
cbb0f765b8 | ||
|
|
726a2fbbc9 | ||
|
|
553b5aa02b | ||
|
|
2ff0d951ed | ||
|
|
1f88925144 | ||
|
|
250433a91a | ||
|
|
eb664f47af | ||
|
|
bc85d4e87b | ||
|
|
7585444ce6 | ||
|
|
a886798a10 | ||
|
|
37975e4eac | ||
|
|
a41b8a6d4f | ||
|
|
b264685052 | ||
|
|
78d0ea6fa5 | ||
|
|
ea4e57b602 | ||
|
|
4789d86fe8 | ||
|
|
09e7d1040e | ||
|
|
dfa1cbba4f |
@@ -67,9 +67,9 @@ This is critical for better file inspection:
|
||||
|
||||
Use git locally for much faster analysis:
|
||||
|
||||
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
|
||||
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
|
||||
|
||||
### Step 1.5: Create Analysis Cache
|
||||
|
||||
|
||||
109
.github/workflows/backport.yaml
vendored
109
.github/workflows/backport.yaml
vendored
@@ -4,10 +4,25 @@ on:
|
||||
pull_request_target:
|
||||
types: [closed, labeled]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to backport'
|
||||
required: true
|
||||
type: string
|
||||
force_rerun:
|
||||
description: 'Force rerun even if backports exist'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
|
||||
if: >
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -15,6 +30,35 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Validate inputs for manual triggers
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
# Validate PR number format
|
||||
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::Invalid PR number format. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR exists and is merged
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR has needs-backport label
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -29,7 +73,7 @@ jobs:
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
# Check for existing backport PRs for this PR number
|
||||
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||
@@ -39,6 +83,13 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For manual triggers with force_rerun, proceed anyway
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Force rerun requested - existing backports will be updated"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found existing backport PRs:"
|
||||
echo "$EXISTING_BACKPORTS"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
@@ -50,8 +101,17 @@ jobs:
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
for label in $(echo "$LABELS" | jq -r '.[].name'); do
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
@@ -75,12 +135,20 @@ jobs:
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
@@ -133,10 +201,18 @@ jobs:
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
|
||||
@@ -165,9 +241,16 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -78,8 +78,8 @@ vite.config.mts.timestamp-*.mjs
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
|
||||
|
||||
# MCP Servers
|
||||
.playwright-mcp/*
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -15,21 +15,32 @@ const config: StorybookConfig = {
|
||||
async viteFinal(config) {
|
||||
// Use dynamic import to avoid CJS deprecation warning
|
||||
const { mergeConfig } = await import('vite')
|
||||
const { default: tailwindcss } = await import('@tailwindcss/vite')
|
||||
|
||||
// Filter out any plugins that might generate import maps
|
||||
if (config.plugins) {
|
||||
config.plugins = config.plugins.filter((plugin: any) => {
|
||||
if (plugin && plugin.name && plugin.name.includes('import-map')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
config.plugins = config.plugins
|
||||
// Type guard: ensure we have valid plugin objects with names
|
||||
.filter(
|
||||
(plugin): plugin is NonNullable<typeof plugin> & { name: string } => {
|
||||
return (
|
||||
plugin !== null &&
|
||||
plugin !== undefined &&
|
||||
typeof plugin === 'object' &&
|
||||
'name' in plugin &&
|
||||
typeof plugin.name === 'string'
|
||||
)
|
||||
}
|
||||
)
|
||||
// Business logic: filter out import-map plugins
|
||||
.filter((plugin) => !plugin.name.includes('import-map'))
|
||||
}
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Replace plugins entirely to avoid inheritance issues
|
||||
plugins: [
|
||||
// Only include plugins we explicitly need for Storybook
|
||||
tailwindcss(),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { setup } from '@storybook/vue3'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
|
||||
import '../src/assets/css/style.css'
|
||||
import { i18n } from '../src/i18n'
|
||||
import '../src/lib/litegraph/public/css/litegraph.css'
|
||||
import { useWidgetStore } from '../src/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
|
||||
import '@/assets/css/style.css'
|
||||
import { i18n } from '@/i18n'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
@@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
// Setup Vue app for Storybook
|
||||
setup((app) => {
|
||||
app.directive('tooltip', Tooltip)
|
||||
|
||||
// Create Pinia instance
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// Initialize stores
|
||||
useColorPaletteStore(pinia)
|
||||
useWidgetStore(pinia)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
@@ -50,8 +46,8 @@ setup((app) => {
|
||||
app.use(ToastService)
|
||||
})
|
||||
|
||||
// Dark theme decorator
|
||||
export const withTheme = (Story: any, context: any) => {
|
||||
// Theme and dialog decorator
|
||||
export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
|
||||
// Apply theme class to document root
|
||||
@@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => {
|
||||
document.body.classList.remove('dark-theme')
|
||||
}
|
||||
|
||||
return Story()
|
||||
return Story(context.args, context)
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, test as base } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { ComfyPage } from '../ComfyPage'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
|
||||
export class SettingDialog {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
class SidebarTab {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
private readonly menuLocator: Locator
|
||||
|
||||
@@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{
|
||||
// so we can look it up to trigger messages
|
||||
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
|
||||
window.WebSocket = class extends window.WebSocket {
|
||||
constructor() {
|
||||
// @ts-expect-error
|
||||
super(...arguments)
|
||||
constructor(
|
||||
...rest: ConstructorParameters<typeof window.WebSocket>
|
||||
) {
|
||||
super(...rest)
|
||||
store[this.url] = this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
import type {
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '../../src/platform/workflow/templates/types/template'
|
||||
|
||||
@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
|
||||
|
||||
// Intercept the prompt queue endpoint
|
||||
let promptNumber = 0
|
||||
comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
route.fulfill({
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
prompt_id: promptNumber,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { SettingParams } from '../../src/platform/settings/types'
|
||||
import type { SettingParams } from '../../src/platform/settings/types'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import { Position } from '@vueuse/core'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Remote COMBO Widget', () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
|
||||
@@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
@@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
@@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import type { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Version Mismatch Warnings', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
/* Test files should not be compiled */
|
||||
"noEmit": true,
|
||||
@@ -9,13 +9,6 @@
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"*.mts",
|
||||
"*.config.js",
|
||||
"browser_tests/**/*.ts",
|
||||
"scripts/**/*.js",
|
||||
"scripts/**/*.ts",
|
||||
"tests-ui/**/*.ts",
|
||||
".storybook/**/*.ts"
|
||||
"**/*.ts",
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import { Plugin } from 'vite'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
interface ShimResult {
|
||||
code: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import glob from 'fast-glob'
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
|
||||
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
|
||||
|
||||
interface ImportMapSource {
|
||||
name: string
|
||||
|
||||
@@ -153,5 +153,14 @@ export default defineConfig([
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['tests-ui/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ disallowTypeAnnotations: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.27.5",
|
||||
"version": "1.28.0",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
--color-charcoal-700: #202121;
|
||||
--color-charcoal-800: #171718;
|
||||
|
||||
--color-neutral-550: #636363;
|
||||
|
||||
--color-stone-100: #444444;
|
||||
--color-stone-200: #828282;
|
||||
--color-stone-300: #bbbbbb;
|
||||
@@ -103,6 +105,10 @@
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
--color-coral-red-600: #973a40;
|
||||
--color-coral-red-500: #c53f49;
|
||||
--color-coral-red-400: #dd424e;
|
||||
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-if="props.device" class="grid grid-cols-2 gap-2">
|
||||
<template v-for="col in deviceColumns" :key="col.field">
|
||||
<div class="font-medium">
|
||||
{{ col.header }}
|
||||
@@ -9,6 +9,9 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="text-red-500">
|
||||
{{ $t('g.deviceNotAvailable') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -16,7 +19,7 @@ import type { DeviceStats } from '@/schemas/apiSchema'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
device: DeviceStats
|
||||
device: DeviceStats | undefined
|
||||
}>()
|
||||
|
||||
const deviceColumns: { field: keyof DeviceStats; header: string }[] = [
|
||||
@@ -29,6 +32,10 @@ const deviceColumns: { field: keyof DeviceStats; header: string }[] = [
|
||||
]
|
||||
|
||||
const formatValue = (value: any, field: string) => {
|
||||
if (value === undefined || value === null) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
if (
|
||||
['vram_total', 'vram_free', 'torch_vram_total', 'torch_vram_free'].includes(
|
||||
field
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="absolute inset-0"
|
||||
/>
|
||||
<img
|
||||
v-show="isImageLoaded"
|
||||
v-if="cachedSrc"
|
||||
ref="imageRef"
|
||||
:src="cachedSrc"
|
||||
:alt="alt"
|
||||
@@ -77,8 +77,8 @@ const shouldLoad = computed(() => isIntersecting.value)
|
||||
|
||||
watch(
|
||||
shouldLoad,
|
||||
async (shouldLoad) => {
|
||||
if (shouldLoad && src && !cachedSrc.value && !hasError.value) {
|
||||
async (shouldLoadVal) => {
|
||||
if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
|
||||
try {
|
||||
const cachedMedia = await getCachedMedia(src)
|
||||
if (cachedMedia.error) {
|
||||
@@ -93,7 +93,7 @@ watch(
|
||||
console.warn('Failed to load cached media:', error)
|
||||
cachedSrc.value = src
|
||||
}
|
||||
} else if (!shouldLoad) {
|
||||
} else if (!shouldLoadVal) {
|
||||
if (cachedSrc.value?.startsWith('blob:')) {
|
||||
releaseUrl(src)
|
||||
}
|
||||
|
||||
@@ -20,17 +20,22 @@
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
{{ $t('g.devices') }}
|
||||
</h2>
|
||||
<TabView v-if="props.stats.devices.length > 1">
|
||||
<TabPanel
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
<DeviceInfo v-else :device="props.stats.devices[0]" />
|
||||
<div v-if="props.stats.devices && props.stats.devices.length > 0">
|
||||
<TabView v-if="props.stats.devices.length > 1">
|
||||
<TabPanel
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
<DeviceInfo v-else :device="props.stats.devices[0]" />
|
||||
</div>
|
||||
<div v-else class="text-yellow-600">
|
||||
{{ $t('g.noDevicesDetected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -39,7 +44,7 @@
|
||||
import Divider from 'primevue/divider'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabView from 'primevue/tabview'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
|
||||
import DeviceInfo from '@/components/common/DeviceInfo.vue'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
@@ -71,4 +76,15 @@ const formatValue = (value: any, field: string) => {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Monitor for missing devices scenario for debugging
|
||||
watchEffect(() => {
|
||||
if (!props.stats?.devices || props.stats.devices.length === 0) {
|
||||
console.warn('[SystemStatsPanel] No devices available in SystemStats:', {
|
||||
hasDevices: !!props.stats?.devices,
|
||||
deviceCount: props.stats?.devices?.length || 0,
|
||||
statsStructure: props.stats ? Object.keys(props.stats) : null
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -138,7 +138,7 @@ const allMissingNodesInstalled = computed(() => {
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled) {
|
||||
if (allInstalled && showInstallAllButton.value) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -96,7 +96,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
@@ -118,8 +117,9 @@ import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables
|
||||
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/linkInteractions/slotLinkPreviewRenderer'
|
||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
||||
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
|
||||
@@ -124,11 +124,11 @@ import ButtonGroup from 'primevue/buttongroup'
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useZoomControls } from '@/composables/useZoomControls'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -5,12 +5,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
// Mock the composables and services
|
||||
vi.mock('@/composables/graph/useCanvasInteractions', () => ({
|
||||
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
||||
useCanvasInteractions: vi.fn(() => ({
|
||||
handleWheel: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -60,9 +60,9 @@ import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButt
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'
|
||||
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
@@ -170,50 +170,75 @@ export function useSelectionToolboxPosition(
|
||||
}
|
||||
)
|
||||
|
||||
// Watch for dragging state
|
||||
watch(
|
||||
() => canvasStore.canvas?.state?.draggingItems,
|
||||
(dragging) => {
|
||||
if (dragging) {
|
||||
visible.value = false
|
||||
|
||||
if (moreOptionsOpen.value) {
|
||||
const currentSig = buildSelectionSignature(canvasStore)
|
||||
if (currentSig !== moreOptionsSelectionSignature) {
|
||||
moreOptionsSelectionSignature = null
|
||||
}
|
||||
moreOptionsWasOpenBeforeDrag = true
|
||||
moreOptionsOpen.value = false
|
||||
moreOptionsRestorePending.value = !!moreOptionsSelectionSignature
|
||||
if (moreOptionsRestorePending.value) {
|
||||
forceCloseMoreOptionsSignal.value++
|
||||
} else {
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
}
|
||||
} else {
|
||||
moreOptionsRestorePending.value = false
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
}
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectionBounds()
|
||||
const selectionMatches = currentSelectionMatchesSignature(canvasStore)
|
||||
const shouldRestore =
|
||||
moreOptionsWasOpenBeforeDrag &&
|
||||
visible.value &&
|
||||
moreOptionsRestorePending.value &&
|
||||
selectionMatches
|
||||
|
||||
if (shouldRestore) {
|
||||
restoreMoreOptionsSignal.value++
|
||||
} else {
|
||||
moreOptionsRestorePending.value = false
|
||||
}
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
})
|
||||
}
|
||||
const handleDragStateChange = (dragging: boolean) => {
|
||||
if (dragging) {
|
||||
handleDragStart()
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
handleDragEnd()
|
||||
}
|
||||
|
||||
const handleDragStart = () => {
|
||||
visible.value = false
|
||||
|
||||
// Early return if more options wasn't open
|
||||
if (!moreOptionsOpen.value) {
|
||||
moreOptionsRestorePending.value = false
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
return
|
||||
}
|
||||
|
||||
// Handle more options cleanup
|
||||
const currentSig = buildSelectionSignature(canvasStore)
|
||||
const selectionChanged = currentSig !== moreOptionsSelectionSignature
|
||||
|
||||
if (selectionChanged) {
|
||||
moreOptionsSelectionSignature = null
|
||||
}
|
||||
moreOptionsOpen.value = false
|
||||
moreOptionsWasOpenBeforeDrag = true
|
||||
moreOptionsRestorePending.value = !!moreOptionsSelectionSignature
|
||||
|
||||
if (moreOptionsRestorePending.value) {
|
||||
forceCloseMoreOptionsSignal.value++
|
||||
return
|
||||
}
|
||||
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectionBounds()
|
||||
|
||||
const selectionMatches = currentSelectionMatchesSignature(canvasStore)
|
||||
const shouldRestore =
|
||||
moreOptionsWasOpenBeforeDrag &&
|
||||
visible.value &&
|
||||
moreOptionsRestorePending.value &&
|
||||
selectionMatches
|
||||
|
||||
// Single point of assignment for each ref
|
||||
moreOptionsRestorePending.value =
|
||||
shouldRestore && moreOptionsRestorePending.value
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
|
||||
if (shouldRestore) {
|
||||
restoreMoreOptionsSignal.value++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Unified dragging state - combines both LiteGraph and Vue node dragging
|
||||
const isDragging = computed((): boolean => {
|
||||
const litegraphDragging = canvasStore.canvas?.state?.draggingItems ?? false
|
||||
const vueNodeDragging =
|
||||
shouldRenderVueNodes.value && layoutStore.isDraggingVueNodes.value
|
||||
return litegraphDragging || vueNodeDragging
|
||||
})
|
||||
|
||||
watch(isDragging, handleDragStateChange)
|
||||
|
||||
onUnmounted(() => {
|
||||
resetMoreOptionsState()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
|
||||
@@ -42,7 +42,12 @@ export function useManagerState() {
|
||||
)
|
||||
|
||||
// Check command line args first (highest priority)
|
||||
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
|
||||
// --enable-manager flag enables the manager (opposite of old --disable-manager)
|
||||
const hasEnableManager =
|
||||
systemStats.value?.system?.argv?.includes('--enable-manager')
|
||||
|
||||
// If --enable-manager is NOT present, manager is disabled
|
||||
if (!hasEnableManager) {
|
||||
return ManagerUIState.DISABLED
|
||||
}
|
||||
|
||||
|
||||
75
src/constants/desktopDialogs.ts
Normal file
75
src/constants/desktopDialogs.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export interface DialogAction {
|
||||
readonly label: string
|
||||
readonly action: 'openUrl' | 'close' | 'cancel'
|
||||
readonly url?: string
|
||||
readonly severity?: 'danger' | 'primary' | 'secondary' | 'warn'
|
||||
readonly returnValue: string
|
||||
}
|
||||
|
||||
interface DesktopDialog {
|
||||
readonly title: string
|
||||
readonly message: string
|
||||
readonly buttons: DialogAction[]
|
||||
}
|
||||
|
||||
export const DESKTOP_DIALOGS = {
|
||||
/** Shown when a corrupt venv is detected. */
|
||||
reinstallVenv: {
|
||||
title: 'Reinstall ComfyUI (Fresh Start)?',
|
||||
message: `Sorry, we can't launch ComfyUI because some installed packages aren't compatible.
|
||||
|
||||
Click Reinstall to restore ComfyUI and get back up and running.
|
||||
|
||||
Please note: if you've added custom nodes, you'll need to reinstall them after this process.`,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
action: 'openUrl',
|
||||
url: 'https://docs.comfy.org',
|
||||
returnValue: 'openDocs'
|
||||
},
|
||||
{
|
||||
label: 'Reinstall',
|
||||
action: 'close',
|
||||
severity: 'danger',
|
||||
returnValue: 'resetVenv'
|
||||
}
|
||||
]
|
||||
},
|
||||
/** A dialog that is shown when an invalid dialog ID is provided. */
|
||||
invalidDialog: {
|
||||
title: 'Invalid Dialog',
|
||||
message: `Invalid dialog ID was provided.`,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Close',
|
||||
action: 'cancel',
|
||||
returnValue: 'cancel'
|
||||
}
|
||||
]
|
||||
}
|
||||
} as const satisfies { [K: string]: DesktopDialog }
|
||||
|
||||
/** The ID of a desktop dialog. */
|
||||
type DesktopDialogId = keyof typeof DESKTOP_DIALOGS
|
||||
|
||||
/**
|
||||
* Checks if {@link id} is a valid dialog ID.
|
||||
* @param id The string to check
|
||||
* @returns `true` if the ID is a valid dialog ID, otherwise `false`
|
||||
*/
|
||||
function isDialogId(id: unknown): id is DesktopDialogId {
|
||||
return typeof id === 'string' && id in DESKTOP_DIALOGS
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dialog with the given ID.
|
||||
* @param dialogId The ID of the dialog to get
|
||||
* @returns The dialog with the given ID
|
||||
*/
|
||||
export function getDialog(
|
||||
dialogId: string | string[]
|
||||
): DesktopDialog & { id: DesktopDialogId } {
|
||||
const id = isDialogId(dialogId) ? dialogId : 'invalidDialog'
|
||||
return { id, ...structuredClone(DESKTOP_DIALOGS[id]) }
|
||||
}
|
||||
@@ -11,16 +11,16 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import {
|
||||
ComfyLink,
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON
|
||||
type ComfyLink,
|
||||
type ComfyNode,
|
||||
type ComfyWorkflowJSON
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import { type ComfyExtension } from '@/types/comfy'
|
||||
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
|
||||
import { GROUP } from '@/utils/executableGroupNodeDto'
|
||||
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'
|
||||
|
||||
@@ -9,7 +9,7 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { t } from '@/i18n'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import {
|
||||
AnimationItem,
|
||||
AnimationManagerInterface,
|
||||
EventManagerInterface
|
||||
type AnimationItem,
|
||||
type AnimationManagerInterface,
|
||||
type EventManagerInterface
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
export class AnimationManager implements AnimationManagerInterface {
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import {
|
||||
CameraManagerInterface,
|
||||
CameraState,
|
||||
CameraType,
|
||||
EventManagerInterface,
|
||||
NodeStorageInterface
|
||||
type CameraManagerInterface,
|
||||
type CameraState,
|
||||
type CameraType,
|
||||
type EventManagerInterface,
|
||||
type NodeStorageInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class CameraManager implements CameraManagerInterface {
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import {
|
||||
ControlsManagerInterface,
|
||||
EventManagerInterface,
|
||||
NodeStorageInterface
|
||||
type ControlsManagerInterface,
|
||||
type EventManagerInterface,
|
||||
type NodeStorageInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class ControlsManager implements ControlsManagerInterface {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventCallback, EventManagerInterface } from './interfaces'
|
||||
import { type EventCallback, type EventManagerInterface } from './interfaces'
|
||||
|
||||
export class EventManager implements EventManagerInterface {
|
||||
private listeners: { [key: string]: EventCallback[] } = {}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { EventManagerInterface, LightingManagerInterface } from './interfaces'
|
||||
import {
|
||||
type EventManagerInterface,
|
||||
type LightingManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class LightingManager implements LightingManagerInterface {
|
||||
lights: THREE.Light[] = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
import { CameraManager } from './CameraManager'
|
||||
import { ControlsManager } from './ControlsManager'
|
||||
@@ -16,11 +16,11 @@ import { SceneManager } from './SceneManager'
|
||||
import { SceneModelManager } from './SceneModelManager'
|
||||
import { ViewHelperManager } from './ViewHelperManager'
|
||||
import {
|
||||
CameraState,
|
||||
CaptureResult,
|
||||
Load3DOptions,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
type CameraState,
|
||||
type CaptureResult,
|
||||
type Load3DOptions,
|
||||
type MaterialMode,
|
||||
type UpDirection
|
||||
} from './interfaces'
|
||||
|
||||
class Load3d {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { AnimationManager } from './AnimationManager'
|
||||
import Load3d from './Load3d'
|
||||
import { Load3DOptions } from './interfaces'
|
||||
import { type Load3DOptions } from './interfaces'
|
||||
|
||||
class Load3dAnimation extends Load3d {
|
||||
private animationManager: AnimationManager
|
||||
|
||||
@@ -9,9 +9,9 @@ import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
import {
|
||||
EventManagerInterface,
|
||||
LoaderManagerInterface,
|
||||
ModelManagerInterface
|
||||
type EventManagerInterface,
|
||||
type LoaderManagerInterface,
|
||||
type ModelManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class LoaderManager implements LoaderManagerInterface {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { NodeStorageInterface } from './interfaces'
|
||||
import { type NodeStorageInterface } from './interfaces'
|
||||
|
||||
export class NodeStorage implements NodeStorageInterface {
|
||||
private node: LGraphNode
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
|
||||
import {
|
||||
type EventManagerInterface,
|
||||
type PreviewManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class PreviewManager implements PreviewManagerInterface {
|
||||
previewCamera: THREE.Camera
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { EventManagerInterface } from './interfaces'
|
||||
import { type EventManagerInterface } from './interfaces'
|
||||
|
||||
export class RecordingManager {
|
||||
private mediaRecorder: MediaRecorder | null = null
|
||||
|
||||
@@ -2,7 +2,10 @@ import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import { EventManagerInterface, SceneManagerInterface } from './interfaces'
|
||||
import {
|
||||
type EventManagerInterface,
|
||||
type SceneManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class SceneManager implements SceneManagerInterface {
|
||||
scene: THREE.Scene
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as THREE from 'three'
|
||||
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
|
||||
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
|
||||
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
|
||||
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
|
||||
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
|
||||
@@ -11,11 +11,11 @@ import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShad
|
||||
import { ConditionalLineMaterial } from './conditional-lines/Lines2/ConditionalLineMaterial'
|
||||
import { ConditionalLineSegmentsGeometry } from './conditional-lines/Lines2/ConditionalLineSegmentsGeometry'
|
||||
import {
|
||||
EventManagerInterface,
|
||||
Load3DOptions,
|
||||
MaterialMode,
|
||||
ModelManagerInterface,
|
||||
UpDirection
|
||||
type EventManagerInterface,
|
||||
type Load3DOptions,
|
||||
type MaterialMode,
|
||||
type ModelManagerInterface,
|
||||
type UpDirection
|
||||
} from './interfaces'
|
||||
|
||||
export class SceneModelManager implements ModelManagerInterface {
|
||||
|
||||
@@ -2,7 +2,10 @@ import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
|
||||
import { NodeStorageInterface, ViewHelperManagerInterface } from './interfaces'
|
||||
import {
|
||||
type NodeStorageInterface,
|
||||
type ViewHelperManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class ViewHelperManager implements ViewHelperManagerInterface {
|
||||
viewHelper: ViewHelper = {} as ViewHelper
|
||||
|
||||
@@ -2,13 +2,13 @@ import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
export type Load3DNodeType = 'Load3D' | 'Preview3D'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
|
||||
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
|
||||
*/
|
||||
import { app } from '@/scripts/app'
|
||||
import { DOMWidget } from '@/scripts/domWidget'
|
||||
import { type DOMWidget } from '@/scripts/domWidget'
|
||||
import { ComfyWidgets } from '@/scripts/widgets'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { useAudioService } from '@/services/audioService'
|
||||
import { NodeLocatorId } from '@/types'
|
||||
import { type NodeLocatorId } from '@/types'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ComfyNodeDef,
|
||||
InputSpec,
|
||||
type ComfyNodeDef,
|
||||
type InputSpec,
|
||||
isComboInputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ import ruCommands from './locales/ru/commands.json' with { type: 'json' }
|
||||
import ru from './locales/ru/main.json' with { type: 'json' }
|
||||
import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' }
|
||||
import ruSettings from './locales/ru/settings.json' with { type: 'json' }
|
||||
import trCommands from './locales/tr/commands.json' with { type: 'json' }
|
||||
import tr from './locales/tr/main.json' with { type: 'json' }
|
||||
import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' }
|
||||
import trSettings from './locales/tr/settings.json' with { type: 'json' }
|
||||
import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' }
|
||||
import zhTW from './locales/zh-TW/main.json' with { type: 'json' }
|
||||
import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' }
|
||||
@@ -55,7 +59,8 @@ const messages = {
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
fr: buildLocale(fr, frNodes, frCommands, frSettings),
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings),
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings)
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings),
|
||||
tr: buildLocale(tr, trNodes, trCommands, trSettings)
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
|
||||
import type { ISubgraphInput } from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
ISubgraphInput,
|
||||
IWidgetLocator
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
ISlotType,
|
||||
@@ -78,9 +81,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const existingInput = this.inputs.find((i) => i.name == name)
|
||||
if (existingInput) {
|
||||
const linkId = subgraphInput.linkIds[0]
|
||||
const { inputNode } = subgraph.links[linkId].resolve(subgraph)
|
||||
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
|
||||
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
|
||||
if (widget) this.#setWidget(subgraphInput, existingInput, widget)
|
||||
if (widget)
|
||||
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
|
||||
return
|
||||
}
|
||||
const input = this.addInput(name, type)
|
||||
@@ -185,13 +189,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
subgraphInput.events.addEventListener(
|
||||
'input-connected',
|
||||
() => {
|
||||
(e) => {
|
||||
if (input._widget) return
|
||||
|
||||
const widget = subgraphInput._widget
|
||||
if (!widget) return
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
const widgetLocator = e.detail.input.widget
|
||||
this.#setWidget(subgraphInput, input, widget, widgetLocator)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
@@ -301,7 +306,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const widget = resolved.inputNode.getWidgetFromSlot(resolved.input)
|
||||
if (!widget) continue
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
this.#setWidget(subgraphInput, input, widget, resolved.input.widget)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -310,11 +315,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
#setWidget(
|
||||
subgraphInput: Readonly<SubgraphInput>,
|
||||
input: INodeInputSlot,
|
||||
widget: Readonly<IBaseWidget>
|
||||
widget: Readonly<IBaseWidget>,
|
||||
inputWidget: IWidgetLocator | undefined
|
||||
) {
|
||||
// Use the first matching widget
|
||||
const targetWidget = toConcreteWidget(widget, this)
|
||||
const promotedWidget = targetWidget.createCopyForNode(this)
|
||||
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
|
||||
this
|
||||
)
|
||||
|
||||
Object.assign(promotedWidget, {
|
||||
get name() {
|
||||
@@ -372,11 +379,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// NOTE: This code creates linked chains of prototypes for passing across
|
||||
// multiple levels of subgraphs. As part of this, it intentionally avoids
|
||||
// creating new objects. Have care when making changes.
|
||||
const backingInput =
|
||||
targetWidget.node.findInputSlot(widget.name, true)?.widget ?? {}
|
||||
input.widget ??= { name: subgraphInput.name }
|
||||
input.widget.name = subgraphInput.name
|
||||
Object.setPrototypeOf(input.widget, backingInput)
|
||||
if (inputWidget) Object.setPrototypeOf(input.widget, inputWidget)
|
||||
|
||||
input._widget = promotedWidget
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ Add your language code to the `outputLocales` array:
|
||||
```javascript
|
||||
module.exports = defineConfig({
|
||||
// ... existing config
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'], // Add your language here
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'tr'], // Add your language here
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -11,6 +11,7 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
|
||||
- ko (한국어)
|
||||
- fr (Français)
|
||||
- es (Español)
|
||||
- tr (Türkçe)
|
||||
|
||||
## How to Add a New Language
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"insert": "Insert",
|
||||
"systemInfo": "System Info",
|
||||
"devices": "Devices",
|
||||
"deviceNotAvailable": "Device information is not available.",
|
||||
"noDevicesDetected": "No devices detected. This may occur if no GPU devices are available or device enumeration failed.",
|
||||
"about": "About",
|
||||
"add": "Add",
|
||||
"confirm": "Confirm",
|
||||
@@ -1862,5 +1864,17 @@
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
},
|
||||
"assetBrowser": {
|
||||
"assets": "Assets",
|
||||
"browseAssets": "Browse Assets",
|
||||
"noAssetsFound": "No assets found",
|
||||
"tryAdjustingFilters": "Try adjusting your search or filters",
|
||||
"loadingModels": "Loading {type}...",
|
||||
"connectionError": "Please check your connection and try again",
|
||||
"noModelsInFolder": "No {type} available in this folder",
|
||||
"searchAssetsPlaceholder": "Search assets...",
|
||||
"allModels": "All Models",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
312
src/locales/tr/commands.json
Normal file
312
src/locales/tr/commands.json
Normal file
@@ -0,0 +1,312 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "Güncellemeleri Kontrol Et"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Özel Düğümler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Girişler Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Kayıtlar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "extra_model_paths.yaml dosyasını aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Modeller Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Çıktılar Klasörünü Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Geliştirici Araçlarını Aç"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Masaüstü Kullanıcı Kılavuzu"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Çık"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Yeniden Yükle"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Yeniden Başlat"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Şablonlara Gözat"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Seçili Öğeleri Sil"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Görünümü seçili düğümlere sığdır"
|
||||
},
|
||||
"Comfy_Canvas_Lock": {
|
||||
"label": "Tuvali Kilitle"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Seçili Düğümleri Aşağı Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Seçili Düğümleri Sola Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Seçili Düğümleri Sağa Taşı"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Seçili Düğümleri Yukarı Taşı"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Görünümü Sıfırla"
|
||||
},
|
||||
"Comfy_Canvas_Resize": {
|
||||
"label": "Seçili Düğümleri Yeniden Boyutlandır"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLinkVisibility": {
|
||||
"label": "Tuval Bağlantı Görünürlüğünü Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Tuval Kilidini Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Mini Haritayı Aç/Kapat"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "Seçili Öğeleri Sabitle/Sabitlemeyi Kaldır"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Seçili Düğümleri Atla/Geri Al"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "Seçili Düğümleri Daralt/Genişlet"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "Seçili Düğümleri Sessize Al/Sesi Aç"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "Seçili Düğümleri Sabitle/Sabitlemeyi Kaldır"
|
||||
},
|
||||
"Comfy_Canvas_Unlock": {
|
||||
"label": "Tuvalin Kilidini Aç"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "Yakınlaştır"
|
||||
},
|
||||
"Comfy_Canvas_ZoomOut": {
|
||||
"label": "Uzaklaştır"
|
||||
},
|
||||
"Comfy_ClearPendingTasks": {
|
||||
"label": "Bekleyen Görevleri Temizle"
|
||||
},
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "İş Akışını Temizle"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Destekle İletişime Geç"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Model Seçiciyi Göster (Geliştirici)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Mevcut İş Akışını Çoğalt"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "İş Akışını Dışa Aktar"
|
||||
},
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "İş Akışını Dışa Aktar (API Formatı)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Geri Bildirim Ver"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Seçimi Alt Grafiğe Dönüştür"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Alt Grafikten Çık"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Grubu İçeriğe Sığdır"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Seçili Düğümleri Gruplandır"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Seçili Alt Grafiği Aç"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Seçili düğümleri grup düğümüne dönüştür"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "Grup düğümlerini yönet"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "Seçili grup düğümlerinin grubunu çöz"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "ComfyUI Hakkında'yı Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "Comfy-Org Discord'unu Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "ComfyUI Belgelerini Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "ComfyUI Forumunu Aç"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "ComfyUI Sorunlarını Aç"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "Kes"
|
||||
},
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Varsayılan İş Akışını Yükle"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Özel Düğüm Yöneticisi"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Özel Düğümler (Eski)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Yönetici Menüsü (Eski)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Eksik Özel Düğümleri Yükle"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Maske Düzenleyicide Fırça Boyutunu Artır"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Modelleri Boşalt"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Modelleri ve Yürütme Önbelleğini Boşalt"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Yeni Boş İş Akışı"
|
||||
},
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Yönetici"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "İş Akışını Aç"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Alt Grafiği Yayınla"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "İstemi Kuyruğa Al"
|
||||
},
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "İstemi Kuyruğa Al (Ön)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Seçili Çıktı Düğümlerini Kuyruğa Al"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Yinele"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Düğüm Tanımlarını Yenile"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "İş Akışını Kaydet"
|
||||
},
|
||||
"Comfy_SaveWorkflowAs": {
|
||||
"label": "İş Akışını Farklı Kaydet"
|
||||
},
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Ayarlar İletişim Kutusunu Göster"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Tuval Performansı"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Yardım Merkezi"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Temayı Değiştir (Karanlık/Açık)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "Geri Al"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Giriş Yapma İletişim Kutusunu Aç"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Çıkış Yap"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Mevcut İş Akışını Kapat"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "Sonraki Açılan İş Akışı"
|
||||
},
|
||||
"Workspace_PreviousOpenedWorkflow": {
|
||||
"label": "Önceki Açılan İş Akışı"
|
||||
},
|
||||
"Workspace_SearchBox_Toggle": {
|
||||
"label": "Arama Kutusunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "Alt Paneli Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "Tuş Atamaları İletişim Kutusunu Göster"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "Terminal Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "Kayıtlar Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "Temel Alt Paneli Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "Görünüm Kontrolleri Alt Panelini Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Odak Modunu Aç/Kapat"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Model Kütüphanesi"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Düğüm Kütüphanesi"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "Kuyruk"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
|
||||
"tooltip": "İş Akışları"
|
||||
}
|
||||
}
|
||||
1800
src/locales/tr/main.json
Normal file
1800
src/locales/tr/main.json
Normal file
File diff suppressed because it is too large
Load Diff
8653
src/locales/tr/nodeDefs.json
Normal file
8653
src/locales/tr/nodeDefs.json
Normal file
File diff suppressed because it is too large
Load Diff
424
src/locales/tr/settings.json
Normal file
424
src/locales/tr/settings.json
Normal file
@@ -0,0 +1,424 @@
|
||||
{
|
||||
"Comfy-Desktop_AutoUpdate": {
|
||||
"name": "Güncellemeleri otomatik olarak kontrol et"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Anonim kullanım metrikleri gönder"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi Yükleme Yansısı",
|
||||
"tooltip": "Varsayılan pip yükleme yansısı"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python Yükleme Yansısı",
|
||||
"tooltip": "Yönetilen Python kurulumları Astral python-build-standalone projesinden indirilir. Bu değişken, Python kurulumları için farklı bir kaynak kullanmak üzere bir yansıma URL'sine ayarlanabilir. Sağlanan URL, örneğin https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'deki https://github.com/astral-sh/python-build-standalone/releases/download'ın yerini alacaktır. Dağıtımlar, file:// URL şeması kullanılarak yerel bir dizinden okunabilir."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch Yükleme Yansısı",
|
||||
"tooltip": "Pytorch için Pip yükleme yansısı"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Pencere Stili",
|
||||
"tooltip": "Özel: Sistem başlık çubuğunu ComfyUI'nin Üst menüsüyle değiştirin",
|
||||
"options": {
|
||||
"default": "varsayılan",
|
||||
"custom": "özel"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "Tuval arka plan resmi",
|
||||
"tooltip": "Tuval arka planı için resim URL'si. Çıktılar panelindeki bir resme sağ tıklayıp \"Arka Plan Olarak Ayarla\"yı seçerek kullanabilir veya yükleme düğmesini kullanarak kendi resminizi yükleyebilirsiniz."
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "Tuval Gezinme Modu",
|
||||
"options": {
|
||||
"Standard (New)": "Standart (Yeni)",
|
||||
"Drag Navigation": "Sürükleyerek Gezinme"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "Seçim araç kutusunu göster"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "İş akışını temizlerken onay iste"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "Geliştirici modu seçeneklerini etkinleştir (API kaydetme, vb.)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "Varsayılan ondalık sayı widget yuvarlamasını devre dışı bırak.",
|
||||
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir) Arka uçtaki düğüm tarafından yuvarlama ayarlandığında yuvarlama devre dışı bırakılamaz."
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "Düğüm widget kaydırıcılarını devre dışı bırak"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "DOM öğesi kırpmayı etkinleştir (etkinleştirmek performansı düşürebilir)"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "Ctrl+yukarı/aşağı hassasiyeti"
|
||||
},
|
||||
"Comfy_EnableTooltips": {
|
||||
"name": "Araç İpuçlarını Etkinleştir"
|
||||
},
|
||||
"Comfy_EnableWorkflowViewRestore": {
|
||||
"name": "İş akışlarında tuval konumunu ve yakınlaştırma seviyesini kaydet ve geri yükle"
|
||||
},
|
||||
"Comfy_FloatRoundingPrecision": {
|
||||
"name": "Ondalık sayı widget yuvarlama ondalık basamakları [0 = otomatik].",
|
||||
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir)"
|
||||
},
|
||||
"Comfy_Graph_CanvasInfo": {
|
||||
"name": "Sol alt köşede tuval bilgilerini göster (fps, vb.)"
|
||||
},
|
||||
"Comfy_Graph_CanvasMenu": {
|
||||
"name": "Grafik tuval menüsünü göster"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Bağlantı orta nokta işaretçileri",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Circle": "Daire",
|
||||
"Arrow": "Ok"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "Tuval yakınlaştırma hızı"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "Düzenlemek için grup başlığına çift tıkla"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "Seçili düğümleri gruplandırma dolgusu"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "Bağlantı bırakıldığında eylem (Değiştirici yok)",
|
||||
"options": {
|
||||
"context menu": "bağlam menüsü",
|
||||
"search box": "arama kutusu",
|
||||
"no action": "eylem yok"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "Bağlantı bırakıldığında eylem (Shift)",
|
||||
"options": {
|
||||
"context menu": "bağlam menüsü",
|
||||
"search box": "arama kutusu",
|
||||
"no action": "eylem yok"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "Bağlantı Oluşturma Modu",
|
||||
"options": {
|
||||
"Straight": "Düz",
|
||||
"Linear": "Doğrusal",
|
||||
"Spline": "Eğri",
|
||||
"Hidden": "Gizli"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "3D Görüntüleyiciyi Etkinleştir (Beta)",
|
||||
"tooltip": "Seçili düğümler için 3D Görüntüleyiciyi (Beta) etkinleştirir. Bu özellik, 3D modelleri doğrudan tam boyutlu 3D görüntüleyici içinde görselleştirmenize ve etkileşimde bulunmanıza olanak tanır."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Başlangıç Arka Plan Rengi",
|
||||
"tooltip": "3D sahnenin varsayılan arka plan rengini kontrol eder. Bu ayar, yeni bir 3D widget oluşturulduğunda arka plan görünümünü belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Başlangıç Kamera Tipi",
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda kameranın varsayılan olarak perspektif mi yoksa ortografik mi olacağını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir.",
|
||||
"options": {
|
||||
"perspective": "perspektif",
|
||||
"orthographic": "ortografik"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Işık Ayarlama Artışı",
|
||||
"tooltip": "3D sahnelerde ışık yoğunluğunu ayarlarken artış boyutunu kontrol eder. Daha küçük bir adım değeri, aydınlatma ayarlamaları üzerinde daha ince kontrol sağlarken, daha büyük bir değer ayarlama başına daha belirgin değişikliklere neden olur."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Başlangıç Işık Yoğunluğu",
|
||||
"tooltip": "3D sahnedeki aydınlatmanın varsayılan parlaklık seviyesini ayarlar. Bu değer, yeni bir 3D widget oluşturulduğunda ışıkların nesneleri ne kadar yoğun aydınlatacağını belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Maksimum Işık Yoğunluğu",
|
||||
"tooltip": "3D sahneler için izin verilen maksimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek üst parlaklık sınırını tanımlar."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Minimum Işık Yoğunluğu",
|
||||
"tooltip": "3D sahneler için izin verilen minimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek alt parlaklık sınırını tanımlar."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Başlangıç Izgara Görünürlüğü",
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda ızgaranın varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Başlangıç Önizleme Görünürlüğü",
|
||||
"tooltip": "Yeni bir 3D widget oluşturulduğunda önizleme ekranının varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Dil"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "Fırça ayar hızı çarpanı",
|
||||
"tooltip": "Ayarlama sırasında fırça boyutunun ve sertliğinin ne kadar hızlı değiştiğini kontrol eder. Daha yüksek değerler daha hızlı değişiklikler anlamına gelir."
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "Fırça ayarını baskın eksene kilitle",
|
||||
"tooltip": "Etkinleştirildiğinde, fırça ayarları yalnızca daha fazla hareket ettiğiniz yöne bağlı olarak boyutu VEYA sertliği etkileyecektir"
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "Yeni maske düzenleyiciyi kullan",
|
||||
"tooltip": "Yeni maske düzenleyici arayüzüne geç"
|
||||
},
|
||||
"Comfy_ModelLibrary_AutoLoadAll": {
|
||||
"name": "Tüm model klasörlerini otomatik olarak yükle",
|
||||
"tooltip": "Doğruysa, model kütüphanesini açar açmaz tüm klasörler yüklenecektir (bu, yüklenirken gecikmelere neden olabilir). Yanlışsa, kök düzeyindeki model klasörleri yalnızca üzerlerine tıkladığınızda yüklenecektir."
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "Model kütüphanesi ağaç görünümünde hangi adın görüntüleneceği",
|
||||
"tooltip": "Model listesinde ham dosya adının (dizin veya \".safetensors\" uzantısı olmadan) basitleştirilmiş bir görünümünü oluşturmak için \"dosyaadı\"nı seçin. Yapılandırılabilir model meta veri başlığını görüntülemek için \"başlık\"ı seçin.",
|
||||
"options": {
|
||||
"filename": "dosyaadı",
|
||||
"title": "başlık"
|
||||
}
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "Görüntü önizlemesinin altında genişlik × yüksekliği göster"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "Bağlantıyı otomatik olarak düğüm yuvasına yapıştır",
|
||||
"tooltip": "Bir bağlantıyı bir düğümün üzerine sürüklerken, bağlantı otomatik olarak düğüm üzerindeki uygun bir giriş yuvasına yapışır"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "Düğümleri silerken tüm bağlantıları koru",
|
||||
"tooltip": "Bir düğümü silerken, tüm giriş ve çıkış bağlantılarını yeniden bağlamaya çalışın (silinen düğümü atlayarak)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "Düzenlemek için düğüm başlığına çift tıkla"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "Orta tıklama yeni bir Yeniden Yönlendirme düğümü oluşturur"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "Düğüm opaklığı"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "Aramada kullanımdan kaldırılmış düğümleri göster",
|
||||
"tooltip": "Kullanımdan kaldırılmış düğümler arayüzde varsayılan olarak gizlidir, ancak bunları kullanan mevcut iş akışlarında işlevsel kalır."
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "Aramada deneysel düğümleri göster",
|
||||
"tooltip": "Deneysel düğümler arayüzde bu şekilde işaretlenmiştir ve gelecekteki sürümlerde önemli değişikliklere veya kaldırılmaya tabi olabilir. Üretim iş akışlarında dikkatli kullanın"
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "Yapıştırma düğümü vurgular",
|
||||
"tooltip": "Uygun giriş yuvasına sahip bir düğümün üzerine bir bağlantı sürüklerken, düğümü vurgulayın"
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "Düğüm ID rozeti modu",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "Düğüm yaşam döngüsü rozeti modu",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "Düğüm kaynak rozeti modu",
|
||||
"options": {
|
||||
"None": "Yok",
|
||||
"Show all": "Tümünü göster",
|
||||
"Hide built-in": "Yerleşik olanı gizle"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "API düğüm fiyatlandırma rozetini göster"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Düğüm arama kutusu uygulaması",
|
||||
"options": {
|
||||
"default": "varsayılan",
|
||||
"litegraph (legacy)": "litegraph (eski)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "Düğüm önizlemesi",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "Arama sonuçlarında düğüm kategorisini göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "Arama sonuçlarında düğüm kimliği adını göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "Arama sonuçlarında düğüm sıklığını göster",
|
||||
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "Düğüm öneri sayısı",
|
||||
"tooltip": "Yalnızca litegraph arama kutusu/bağlam menüsü için"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Sürüm güncellemelerini göster",
|
||||
"tooltip": "Yeni modeller ve önemli yeni özellikler için güncellemeleri göster."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "İşaretçi tıklama kayma gecikmesi",
|
||||
"tooltip": "Bir işaretçi düğmesine bastıktan sonra, bu, işaretçi hareketinin göz ardı edilebileceği maksimum süredir (milisaniye cinsinden).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
|
||||
},
|
||||
"Comfy_Pointer_ClickDrift": {
|
||||
"name": "İşaretçi tıklama kayması (maksimum mesafe)",
|
||||
"tooltip": "İşaretçi bir düğmeyi basılı tutarken bu mesafeden daha fazla hareket ederse, bu sürükleme olarak kabul edilir (tıklama yerine).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
|
||||
},
|
||||
"Comfy_Pointer_DoubleClickTime": {
|
||||
"name": "Çift tıklama aralığı (maksimum)",
|
||||
"tooltip": "Çift tıklamanın iki tıklaması arasındaki milisaniye cinsinden maksimum süre. Bu değeri artırmak, çift tıklamaların bazen kaydedilmemesi durumunda yardımcı olabilir."
|
||||
},
|
||||
"Comfy_PreviewFormat": {
|
||||
"name": "Önizleme görüntü formatı",
|
||||
"tooltip": "Görüntü widget'ında bir önizleme görüntülerken, onu hafif bir görüntüye dönüştürün, örn. webp, jpeg, webp;50, vb."
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "İş akışını kaydederken dosya adı iste"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "Kuyruk geçmişi boyutu",
|
||||
"tooltip": "Kuyruk geçmişinde gösterilen maksimum görev sayısı."
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "Toplu iş sayısı sınırı",
|
||||
"tooltip": "Tek bir düğme tıklamasıyla kuyruğa eklenen maksimum görev sayısı"
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "Kenar çubuğu konumu",
|
||||
"options": {
|
||||
"left": "sol",
|
||||
"right": "sağ"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_Size": {
|
||||
"name": "Kenar çubuğu boyutu",
|
||||
"options": {
|
||||
"normal": "normal",
|
||||
"small": "küçük"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Birleşik kenar çubuğu genişliği"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Izgaraya yapıştırma boyutu",
|
||||
"tooltip": "Shift tuşunu basılı tutarken düğümleri sürükleyip yeniden boyutlandırırken ızgaraya hizalanacaklar, bu o ızgaranın boyutunu kontrol eder."
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "Metin alanı widget yazı tipi boyutu"
|
||||
},
|
||||
"Comfy_TextareaWidget_Spellcheck": {
|
||||
"name": "Metin alanı widget yazım denetimi"
|
||||
},
|
||||
"Comfy_TreeExplorer_ItemPadding": {
|
||||
"name": "Ağaç gezgini öğe dolgusu"
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "Yeni menüyü kullan",
|
||||
"tooltip": "Menü çubuğu konumu. Mobil cihazlarda menü her zaman üstte gösterilir.",
|
||||
"options": {
|
||||
"Disabled": "Devre dışı",
|
||||
"Top": "Üst",
|
||||
"Bottom": "Alt"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "İş akışlarını doğrula"
|
||||
},
|
||||
"Comfy_VueNodes_Enabled": {
|
||||
"name": "Vue düğüm oluşturmayı etkinleştir",
|
||||
"tooltip": "Düğümleri tuval öğeleri yerine Vue bileşenleri olarak oluşturun. Deneysel özellik."
|
||||
},
|
||||
"Comfy_VueNodes_Widgets": {
|
||||
"name": "Vue widget'larını etkinleştir",
|
||||
"tooltip": "Widget'ları Vue düğümleri içinde Vue bileşenleri olarak oluşturun."
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "Widget kontrol modu",
|
||||
"tooltip": "Widget değerlerinin ne zaman güncelleneceğini (rastgele/artırma/azaltma), istem kuyruğa alınmadan önce veya sonra kontrol eder.",
|
||||
"options": {
|
||||
"before": "önce",
|
||||
"after": "sonra"
|
||||
}
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "Pencereyi kapatırken onay göster"
|
||||
},
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "Otomatik Kaydet",
|
||||
"options": {
|
||||
"off": "kapalı",
|
||||
"after delay": "gecikmeden sonra"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
"name": "Otomatik Kaydetme Gecikmesi (ms)",
|
||||
"tooltip": "Yalnızca Otomatik Kaydetme \"gecikmeden sonra\" olarak ayarlandığında geçerlidir."
|
||||
},
|
||||
"Comfy_Workflow_ConfirmDelete": {
|
||||
"name": "İş akışlarını silerken onay göster"
|
||||
},
|
||||
"Comfy_Workflow_Persist": {
|
||||
"name": "İş akışı durumunu koru ve sayfayı (yeniden) yüklediğinde geri yükle"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingModelsWarning": {
|
||||
"name": "Eksik model uyarısını göster"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "Eksik düğüm uyarısını göster"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "İş akışını kaydederken düğüm kimliklerini sırala"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "Açılan iş akışları konumu",
|
||||
"options": {
|
||||
"Sidebar": "Kenar Çubuğu",
|
||||
"Topbar": "Üst Çubuk",
|
||||
"Topbar (2nd-row)": "Üst Çubuk (2. sıra)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_MinFontSizeForLOD": {
|
||||
"name": "Yakınlaştırma Düğümü Ayrıntı Seviyesi - yazı tipi boyutu eşiği",
|
||||
"tooltip": "Düğümlerin ne zaman düşük kaliteli LOD oluşturmaya geçeceğini kontrol eder. Ne zaman geçiş yapılacağını belirlemek için piksel cinsinden yazı tipi boyutunu kullanır. Devre dışı bırakmak için 0'a ayarlayın. 1-24 arasındaki değerler LOD için minimum yazı tipi boyutu eşiğini ayarlar - daha yüksek değerler (24 piksel) = uzaklaştırırken düğümleri daha erken basitleştirilmiş oluşturmaya geçirin, daha düşük değerler (1 piksel) = tam düğüm kalitesini daha uzun süre koruyun."
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Maksimum FPS",
|
||||
"tooltip": "Tuvalin saniyede oluşturmasına izin verilen maksimum kare sayısı. Akıcılık pahasına GPU kullanımını sınırlar. 0 ise, ekran yenileme hızı kullanılır. Varsayılan: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Yakınlaştırıldığında düğüm birleşik widget menülerini (listeleri) ölçeklendir"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Yeni düğümleri her zaman küçült",
|
||||
"tooltip": "Oluşturulduğunda düğümleri mümkün olan en küçük boyuta yeniden boyutlandırın. Devre dışı bırakıldığında, yeni eklenen bir düğüm widget değerlerini göstermek için biraz genişletilecektir."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Araç İpucu Gecikmesi"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Yeniden yönlendirme eğri ofseti",
|
||||
"tooltip": "Yeniden yönlendirme merkez noktasından bezier kontrol noktası ofseti"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Her zaman ızgaraya yapıştır"
|
||||
}
|
||||
}
|
||||
21
src/main.ts
21
src/main.ts
@@ -2,6 +2,11 @@ import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import {
|
||||
browserLocalPersistence,
|
||||
browserSessionPersistence,
|
||||
indexedDBLocalPersistence
|
||||
} from 'firebase/auth'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,7 +14,7 @@ import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { createApp } from 'vue'
|
||||
import { VueFire, VueFireAuth } from 'vuefire'
|
||||
import { VueFire, VueFireAuthWithDependencies } from 'vuefire'
|
||||
|
||||
import { FIREBASE_CONFIG } from '@/config/firebase'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
@@ -66,6 +71,18 @@ app
|
||||
.use(i18n)
|
||||
.use(VueFire, {
|
||||
firebaseApp,
|
||||
modules: [VueFireAuth()]
|
||||
modules: [
|
||||
// Configure Firebase Auth persistence: localStorage first, IndexedDB last.
|
||||
// Localstorage is preferred to IndexedDB for mobile Safari compatibility.
|
||||
VueFireAuthWithDependencies({
|
||||
dependencies: {
|
||||
persistence: [
|
||||
browserLocalPersistence,
|
||||
browserSessionPersistence,
|
||||
indexedDBLocalPersistence
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
.mount('#vue-app')
|
||||
|
||||
42
src/platform/assets/components/AssetBadgeGroup.vue
Normal file
42
src/platform/assets/components/AssetBadgeGroup.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="absolute bottom-2 right-2 flex flex-wrap justify-end gap-1">
|
||||
<span
|
||||
v-for="badge in badges"
|
||||
:key="badge.label"
|
||||
:class="
|
||||
cn(
|
||||
'px-2 py-1 rounded text-xs font-medium uppercase tracking-wider text-white',
|
||||
getBadgeColor(badge.type)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ badge.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
type AssetBadge = {
|
||||
label: string
|
||||
type: 'type' | 'base' | 'size'
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
badges: AssetBadge[]
|
||||
}>()
|
||||
|
||||
function getBadgeColor(type: AssetBadge['type']): string {
|
||||
switch (type) {
|
||||
case 'type':
|
||||
return 'bg-blue-100/90 dark-theme:bg-blue-100/80'
|
||||
case 'base':
|
||||
return 'bg-success-100/90 dark-theme:bg-success-100/80'
|
||||
case 'size':
|
||||
return 'bg-stone-100/90 dark-theme:bg-charcoal-700/80'
|
||||
default:
|
||||
return 'bg-stone-100/90 dark-theme:bg-charcoal-700/80'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
178
src/platform/assets/components/AssetBrowserModal.stories.ts
Normal file
178
src/platform/assets/components/AssetBrowserModal.stories.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
|
||||
import {
|
||||
createMockAssets,
|
||||
mockAssets
|
||||
} from '@/platform/assets/fixtures/ui-mock-assets'
|
||||
|
||||
// Story arguments interface
|
||||
interface StoryArgs {
|
||||
nodeType: string
|
||||
inputName: string
|
||||
currentValue: string
|
||||
showLeftPanel?: boolean
|
||||
}
|
||||
|
||||
const meta: Meta<StoryArgs> = {
|
||||
title: 'Platform/Assets/AssetBrowserModal',
|
||||
component: AssetBrowserModal,
|
||||
parameters: {
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
argTypes: {
|
||||
nodeType: {
|
||||
control: 'select',
|
||||
options: ['CheckpointLoaderSimple', 'VAELoader', 'ControlNetLoader'],
|
||||
description: 'ComfyUI node type for context'
|
||||
},
|
||||
inputName: {
|
||||
control: 'select',
|
||||
options: ['ckpt_name', 'vae_name', 'control_net_name'],
|
||||
description: 'Widget input name'
|
||||
},
|
||||
currentValue: {
|
||||
control: 'text',
|
||||
description: 'Current selected asset value'
|
||||
},
|
||||
showLeftPanel: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show the left panel with categories'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Modal Layout Stories
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
currentValue: '',
|
||||
showLeftPanel: false
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
console.log('Modal closed')
|
||||
}
|
||||
|
||||
return {
|
||||
...args,
|
||||
onAssetSelect,
|
||||
onClose,
|
||||
assets: mockAssets
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center justify-center min-h-screen bg-stone-200 dark-theme:bg-stone-200 p-4">
|
||||
<AssetBrowserModal
|
||||
:node-type="nodeType"
|
||||
:input-name="inputName"
|
||||
:show-left-panel="showLeftPanel"
|
||||
:assets="assets"
|
||||
@asset-select="onAssetSelect"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
// Story demonstrating single asset type (auto-hides left panel)
|
||||
export const SingleAssetType: Story = {
|
||||
args: {
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
currentValue: '',
|
||||
showLeftPanel: false
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
console.log('Modal closed')
|
||||
}
|
||||
|
||||
// Create assets with only one type (checkpoints)
|
||||
const singleTypeAssets = createMockAssets(15).map((asset) => ({
|
||||
...asset,
|
||||
type: 'checkpoint'
|
||||
}))
|
||||
|
||||
return { ...args, onAssetSelect, onClose, assets: singleTypeAssets }
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center justify-center min-h-screen bg-stone-200 dark-theme:bg-stone-200 p-4">
|
||||
<AssetBrowserModal
|
||||
:node-type="nodeType"
|
||||
:input-name="inputName"
|
||||
:show-left-panel="showLeftPanel"
|
||||
:assets="assets"
|
||||
@asset-select="onAssetSelect"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Modal with assets of only one type (checkpoint) - left panel auto-hidden.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with left panel explicitly hidden
|
||||
export const NoLeftPanel: Story = {
|
||||
args: {
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
currentValue: '',
|
||||
showLeftPanel: false
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
console.log('Modal closed')
|
||||
}
|
||||
|
||||
return { ...args, onAssetSelect, onClose, assets: mockAssets }
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center justify-center min-h-screen bg-stone-200 dark-theme:bg-stone-200 p-4">
|
||||
<AssetBrowserModal
|
||||
:node-type="nodeType"
|
||||
:input-name="inputName"
|
||||
:show-left-panel="showLeftPanel"
|
||||
:assets="assets"
|
||||
@asset-select="onAssetSelect"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Modal with left panel explicitly disabled via showLeftPanel=false.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/platform/assets/components/AssetBrowserModal.vue
Normal file
95
src/platform/assets/components/AssetBrowserModal.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<BaseModalLayout
|
||||
data-component-id="AssetBrowserModal"
|
||||
class="size-full max-h-full max-w-full min-w-0"
|
||||
:content-title="contentTitle"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template v-if="shouldShowLeftPanel" #leftPanel>
|
||||
<LeftSidePanel
|
||||
v-model="selectedCategory"
|
||||
data-component-id="AssetBrowserModal-LeftSidePanel"
|
||||
:nav-items="availableCategories"
|
||||
>
|
||||
<template #header-icon>
|
||||
<i-lucide:folder class="size-4" />
|
||||
</template>
|
||||
<template #header-title>{{ $t('assetBrowser.browseAssets') }}</template>
|
||||
</LeftSidePanel>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<SearchBox
|
||||
v-model="searchQuery"
|
||||
size="lg"
|
||||
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
|
||||
class="max-w-96"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<AssetGrid
|
||||
:assets="filteredAssets"
|
||||
@asset-select="handleAssetSelectAndEmit"
|
||||
/>
|
||||
</template>
|
||||
</BaseModalLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeType?: string
|
||||
inputName?: string
|
||||
onSelect?: (assetPath: string) => void
|
||||
onClose?: () => void
|
||||
showLeftPanel?: boolean
|
||||
assets?: AssetItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'asset-select': [asset: AssetDisplayItem]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
// Use AssetBrowser composable for all business logic
|
||||
const {
|
||||
searchQuery,
|
||||
selectedCategory,
|
||||
availableCategories,
|
||||
contentTitle,
|
||||
filteredAssets,
|
||||
selectAsset
|
||||
} = useAssetBrowser(props.assets)
|
||||
|
||||
// Dialog controls panel visibility via prop
|
||||
const shouldShowLeftPanel = computed(() => {
|
||||
return props.showLeftPanel ?? true
|
||||
})
|
||||
|
||||
// Handle close button - call both the prop callback and emit the event
|
||||
const handleClose = () => {
|
||||
props.onClose?.()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// Handle asset selection and emit to parent
|
||||
const handleAssetSelectAndEmit = (asset: AssetDisplayItem) => {
|
||||
selectAsset(asset) // This logs the selection for dev mode
|
||||
emit('asset-select', asset) // Emit the full asset object
|
||||
|
||||
// Call prop callback if provided
|
||||
if (props.onSelect) {
|
||||
props.onSelect(asset.name) // Use asset name as the asset path
|
||||
}
|
||||
}
|
||||
</script>
|
||||
182
src/platform/assets/components/AssetCard.stories.ts
Normal file
182
src/platform/assets/components/AssetCard.stories.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { mockAssets } from '@/platform/assets/fixtures/ui-mock-assets'
|
||||
|
||||
// Use the first mock asset as base and transform it to display format
|
||||
const baseAsset = mockAssets[0]
|
||||
const createAssetData = (
|
||||
overrides: Partial<AssetDisplayItem> = {}
|
||||
): AssetDisplayItem => ({
|
||||
...baseAsset,
|
||||
description:
|
||||
'High-quality realistic images with perfect detail and natural lighting effects for professional photography',
|
||||
formattedSize: '2.1 GB',
|
||||
badges: [
|
||||
{ label: 'checkpoints', type: 'type' },
|
||||
{ label: '2.1 GB', type: 'size' }
|
||||
],
|
||||
stats: {
|
||||
formattedDate: '3/15/25',
|
||||
downloadCount: '1.8k',
|
||||
stars: '4.2k'
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
|
||||
const meta: Meta<typeof AssetCard> = {
|
||||
title: 'Platform/Assets/AssetCard',
|
||||
component: AssetCard,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template:
|
||||
'<div class="p-8 bg-gray-50 dark-theme:bg-gray-900"><story /></div>'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
asset: createAssetData(),
|
||||
interactive: true
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template:
|
||||
'<div class="p-8 bg-gray-50 dark-theme:bg-gray-900 max-w-96"><story /></div>'
|
||||
})
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Default AssetCard with complete data including badges and all stats.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const NonInteractive: Story = {
|
||||
args: {
|
||||
asset: createAssetData(),
|
||||
interactive: false
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template:
|
||||
'<div class="p-8 bg-gray-50 dark-theme:bg-gray-900 max-w-96"><story /></div>'
|
||||
})
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'AssetCard in non-interactive mode - renders as div without button semantics.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EdgeCases: Story = {
|
||||
render: () => ({
|
||||
components: { AssetCard },
|
||||
setup() {
|
||||
const edgeCases = [
|
||||
// Default case for comparison
|
||||
createAssetData({
|
||||
name: 'Complete Data',
|
||||
description: 'Asset with all data present for comparison'
|
||||
}),
|
||||
// No badges
|
||||
createAssetData({
|
||||
id: 'no-badges',
|
||||
name: 'No Badges',
|
||||
description: 'Testing graceful handling when badges are not provided',
|
||||
badges: []
|
||||
}),
|
||||
// No stars
|
||||
createAssetData({
|
||||
id: 'no-stars',
|
||||
name: 'No Stars',
|
||||
description: 'Testing missing stars data gracefully',
|
||||
stats: {
|
||||
downloadCount: '1.8k',
|
||||
formattedDate: '3/15/25'
|
||||
}
|
||||
}),
|
||||
// No downloads
|
||||
createAssetData({
|
||||
id: 'no-downloads',
|
||||
name: 'No Downloads',
|
||||
description: 'Testing missing downloads data gracefully',
|
||||
stats: {
|
||||
stars: '4.2k',
|
||||
formattedDate: '3/15/25'
|
||||
}
|
||||
}),
|
||||
// No date
|
||||
createAssetData({
|
||||
id: 'no-date',
|
||||
name: 'No Date',
|
||||
description: 'Testing missing date data gracefully',
|
||||
stats: {
|
||||
stars: '4.2k',
|
||||
downloadCount: '1.8k'
|
||||
}
|
||||
}),
|
||||
// No stats at all
|
||||
createAssetData({
|
||||
id: 'no-stats',
|
||||
name: 'No Stats',
|
||||
description: 'Testing when all stats are missing',
|
||||
stats: {}
|
||||
}),
|
||||
// Long description
|
||||
createAssetData({
|
||||
id: 'long-desc',
|
||||
name: 'Long Description',
|
||||
description:
|
||||
'This is a very long description that should demonstrate how the component handles text overflow and truncation with ellipsis. The description continues with even more content to ensure we test the 2-line clamp behavior properly and see how it renders when there is significantly more text than can fit in the allocated space.'
|
||||
}),
|
||||
// Minimal data
|
||||
createAssetData({
|
||||
id: 'minimal',
|
||||
name: 'Minimal',
|
||||
description: 'Basic model',
|
||||
tags: ['models'],
|
||||
badges: [],
|
||||
stats: {}
|
||||
})
|
||||
]
|
||||
|
||||
return { edgeCases }
|
||||
},
|
||||
template: `
|
||||
<div class="grid grid-cols-4 gap-6 p-8 bg-gray-50 dark-theme:bg-gray-900">
|
||||
<AssetCard
|
||||
v-for="asset in edgeCases"
|
||||
:key="asset.id"
|
||||
:asset="asset"
|
||||
:interactive="true"
|
||||
@select="(asset) => console.log('Selected:', asset)"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'All AssetCard edge cases in a grid layout to test graceful handling of missing data, badges, stats, and long descriptions.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/platform/assets/components/AssetCard.vue
Normal file
111
src/platform/assets/components/AssetCard.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<component
|
||||
:is="interactive ? 'button' : 'div'"
|
||||
data-component-id="AssetCard"
|
||||
:data-asset-id="asset.id"
|
||||
v-bind="elementProps"
|
||||
:class="
|
||||
cn(
|
||||
// Base layout and container styles (always applied)
|
||||
'rounded-xl overflow-hidden transition-all duration-200',
|
||||
// Button-specific styles
|
||||
interactive && [
|
||||
'appearance-none bg-transparent p-0 m-0 font-inherit text-inherit outline-none cursor-pointer text-left',
|
||||
'bg-ivory-100 border border-gray-300 dark-theme:bg-charcoal-400 dark-theme:border-charcoal-600',
|
||||
'hover:transform hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/10 hover:border-gray-400',
|
||||
'dark-theme:hover:shadow-lg dark-theme:hover:shadow-black/30 dark-theme:hover:border-charcoal-700',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 dark-theme:focus:ring-blue-400'
|
||||
],
|
||||
// Div-specific styles
|
||||
!interactive && [
|
||||
'bg-ivory-100 border border-gray-300',
|
||||
'dark-theme:bg-charcoal-400 dark-theme:border-charcoal-600'
|
||||
]
|
||||
)
|
||||
"
|
||||
@click="interactive && $emit('select', asset)"
|
||||
@keydown.enter="interactive && $emit('select', asset)"
|
||||
>
|
||||
<div class="relative w-full aspect-square overflow-hidden">
|
||||
<div
|
||||
class="w-full h-full bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-600 flex items-center justify-center"
|
||||
></div>
|
||||
<AssetBadgeGroup :badges="asset.badges" />
|
||||
</div>
|
||||
<div class="p-4 h-32 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3
|
||||
:class="
|
||||
cn(
|
||||
'mb-2 m-0 text-base font-semibold overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
'text-slate-800',
|
||||
'dark-theme:text-white'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ asset.name }}
|
||||
</h3>
|
||||
<p
|
||||
:class="
|
||||
cn(
|
||||
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
|
||||
'text-stone-300',
|
||||
'dark-theme:text-stone-200'
|
||||
)
|
||||
"
|
||||
:title="asset.description"
|
||||
>
|
||||
{{ asset.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex gap-4 text-xs',
|
||||
'text-stone-400',
|
||||
'dark-theme:text-stone-300'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span v-if="asset.stats.stars" class="flex items-center gap-1">
|
||||
<i-lucide:star class="size-3" />
|
||||
{{ asset.stats.stars }}
|
||||
</span>
|
||||
<span v-if="asset.stats.downloadCount" class="flex items-center gap-1">
|
||||
<i-lucide:download class="size-3" />
|
||||
{{ asset.stats.downloadCount }}
|
||||
</span>
|
||||
<span v-if="asset.stats.formattedDate" class="flex items-center gap-1">
|
||||
<i-lucide:clock class="size-3" />
|
||||
{{ asset.stats.formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
asset: AssetDisplayItem
|
||||
interactive?: boolean
|
||||
}>()
|
||||
|
||||
const elementProps = computed(() =>
|
||||
props.interactive
|
||||
? {
|
||||
type: 'button',
|
||||
'aria-label': `Select asset ${props.asset.name}`
|
||||
}
|
||||
: {}
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
select: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
</script>
|
||||
102
src/platform/assets/components/AssetFilterBar.vue
Normal file
102
src/platform/assets/components/AssetFilterBar.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div :class="containerClasses" data-component-id="asset-filter-bar">
|
||||
<div :class="leftSideClasses" data-component-id="asset-filter-bar-left">
|
||||
<MultiSelect
|
||||
v-model="fileFormats"
|
||||
label="File formats"
|
||||
:options="fileFormatOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-file-formats"
|
||||
@update:model-value="handleFilterChange"
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
v-model="baseModels"
|
||||
label="Base models"
|
||||
:options="baseModelOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-base-models"
|
||||
@update:model-value="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="rightSideClasses" data-component-id="asset-filter-bar-right">
|
||||
<SingleSelect
|
||||
v-model="sortBy"
|
||||
label="Sort by"
|
||||
:options="sortOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-sort"
|
||||
@update:model-value="handleFilterChange"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:arrow-up-down class="size-3" />
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import type { SelectOption } from '@/components/input/types'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
export interface FilterState {
|
||||
fileFormats: string[]
|
||||
baseModels: string[]
|
||||
sortBy: string
|
||||
}
|
||||
|
||||
const fileFormats = ref<SelectOption[]>([])
|
||||
const baseModels = ref<SelectOption[]>([])
|
||||
const sortBy = ref('name-asc')
|
||||
|
||||
// TODO: Make fileFormatOptions configurable via props or assetService
|
||||
// Should support dynamic file formats based on available assets or server capabilities
|
||||
const fileFormatOptions = [
|
||||
{ name: '.ckpt', value: 'ckpt' },
|
||||
{ name: '.safetensors', value: 'safetensors' },
|
||||
{ name: '.pt', value: 'pt' }
|
||||
]
|
||||
|
||||
// TODO: Make baseModelOptions configurable via props or assetService
|
||||
// Should support dynamic base models based on available assets or server detection
|
||||
const baseModelOptions = [
|
||||
{ name: 'SD 1.5', value: 'sd15' },
|
||||
{ name: 'SD XL', value: 'sdxl' },
|
||||
{ name: 'SD 3.5', value: 'sd35' }
|
||||
]
|
||||
|
||||
// TODO: Make sortOptions configurable via props
|
||||
// Different asset types might need different sorting options
|
||||
const sortOptions = [
|
||||
{ name: 'A-Z', value: 'name-asc' },
|
||||
{ name: 'Z-A', value: 'name-desc' },
|
||||
{ name: 'Recent', value: 'recent' },
|
||||
{ name: 'Popular', value: 'popular' }
|
||||
]
|
||||
|
||||
const emit = defineEmits<{
|
||||
filterChange: [filters: FilterState]
|
||||
}>()
|
||||
|
||||
const containerClasses = cn(
|
||||
'flex gap-4 items-center justify-between',
|
||||
'px-6 pt-2 pb-6'
|
||||
)
|
||||
const leftSideClasses = cn('flex gap-4 items-center')
|
||||
const rightSideClasses = cn('flex items-center')
|
||||
const selectClasses = cn('min-w-32')
|
||||
|
||||
function handleFilterChange() {
|
||||
emit('filterChange', {
|
||||
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
|
||||
baseModels: baseModels.value.map((option: SelectOption) => option.value),
|
||||
sortBy: sortBy.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
70
src/platform/assets/components/AssetGrid.vue
Normal file
70
src/platform/assets/components/AssetGrid.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
data-component-id="AssetGrid"
|
||||
:style="gridStyle"
|
||||
role="grid"
|
||||
aria-label="Asset collection"
|
||||
:aria-rowcount="-1"
|
||||
:aria-colcount="-1"
|
||||
:aria-setsize="assets.length"
|
||||
>
|
||||
<AssetCard
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
:asset="asset"
|
||||
:interactive="true"
|
||||
role="gridcell"
|
||||
@select="$emit('assetSelect', $event)"
|
||||
/>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-if="assets.length === 0"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-full flex flex-col items-center justify-center py-16',
|
||||
'text-stone-300 dark-theme:text-stone-200'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i-lucide:search class="size-10 mb-4" />
|
||||
<h3 class="text-lg font-medium mb-2">
|
||||
{{ $t('assetBrowser.noAssetsFound') }}
|
||||
</h3>
|
||||
<p class="text-sm">{{ $t('assetBrowser.tryAdjustingFilters') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="col-span-full flex items-center justify-center py-16"
|
||||
>
|
||||
<i-lucide:loader
|
||||
:class="
|
||||
cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { createGridStyle } from '@/utils/gridUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineProps<{
|
||||
assets: AssetDisplayItem[]
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
assetSelect: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
|
||||
// Use same grid style as BaseModalLayout
|
||||
const gridStyle = computed(() => createGridStyle())
|
||||
</script>
|
||||
188
src/platform/assets/composables/useAssetBrowser.ts
Normal file
188
src/platform/assets/composables/useAssetBrowser.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import {
|
||||
getAssetBaseModel,
|
||||
getAssetDescription
|
||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
type AssetBadge = {
|
||||
label: string
|
||||
type: 'type' | 'base' | 'size'
|
||||
}
|
||||
|
||||
// Display properties for transformed assets
|
||||
export interface AssetDisplayItem extends AssetItem {
|
||||
description: string
|
||||
formattedSize: string
|
||||
badges: AssetBadge[]
|
||||
stats: {
|
||||
formattedDate?: string
|
||||
downloadCount?: string
|
||||
stars?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Browser composable
|
||||
* Manages search, filtering, asset transformation and selection logic
|
||||
*/
|
||||
export function useAssetBrowser(assets: AssetItem[] = []) {
|
||||
// State
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('all')
|
||||
const sortBy = ref('name')
|
||||
|
||||
// Transform API asset to display asset
|
||||
function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem {
|
||||
// Extract description from metadata or create from tags
|
||||
const typeTag = asset.tags.find((tag) => tag !== 'models')
|
||||
const description =
|
||||
getAssetDescription(asset) ||
|
||||
`${typeTag || t('assetBrowser.unknown')} model`
|
||||
|
||||
// Format file size
|
||||
const formattedSize = formatSize(asset.size)
|
||||
|
||||
// Create badges from tags and metadata
|
||||
const badges: AssetBadge[] = []
|
||||
|
||||
// Type badge from non-root tag
|
||||
if (typeTag) {
|
||||
badges.push({ label: typeTag, type: 'type' })
|
||||
}
|
||||
|
||||
// Base model badge from metadata
|
||||
const baseModel = getAssetBaseModel(asset)
|
||||
if (baseModel) {
|
||||
badges.push({
|
||||
label: baseModel,
|
||||
type: 'base'
|
||||
})
|
||||
}
|
||||
|
||||
// Size badge
|
||||
badges.push({ label: formattedSize, type: 'size' })
|
||||
|
||||
// Create display stats from API data
|
||||
const stats = {
|
||||
formattedDate: new Date(asset.created_at).toLocaleDateString(),
|
||||
downloadCount: undefined, // Not available in API
|
||||
stars: undefined // Not available in API
|
||||
}
|
||||
|
||||
return {
|
||||
...asset,
|
||||
description,
|
||||
formattedSize,
|
||||
badges,
|
||||
stats
|
||||
}
|
||||
}
|
||||
|
||||
// Extract available categories from assets
|
||||
const availableCategories = computed(() => {
|
||||
const categorySet = new Set<string>()
|
||||
|
||||
assets.forEach((asset) => {
|
||||
// Second tag is the category (after 'models' root tag)
|
||||
if (asset.tags.length > 1 && asset.tags[0] === 'models') {
|
||||
categorySet.add(asset.tags[1])
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'all',
|
||||
label: t('assetBrowser.allModels'),
|
||||
icon: 'icon-[lucide--folder]'
|
||||
},
|
||||
...Array.from(categorySet)
|
||||
.sort()
|
||||
.map((category) => ({
|
||||
id: category,
|
||||
label: category.charAt(0).toUpperCase() + category.slice(1),
|
||||
icon: 'icon-[lucide--package]'
|
||||
}))
|
||||
]
|
||||
})
|
||||
|
||||
// Compute content title from selected category
|
||||
const contentTitle = computed(() => {
|
||||
if (selectedCategory.value === 'all') {
|
||||
return t('assetBrowser.allModels')
|
||||
}
|
||||
|
||||
const category = availableCategories.value.find(
|
||||
(cat) => cat.id === selectedCategory.value
|
||||
)
|
||||
return category?.label || t('assetBrowser.assets')
|
||||
})
|
||||
|
||||
// Filter functions
|
||||
const filterByCategory = (category: string) => (asset: AssetItem) => {
|
||||
if (category === 'all') return true
|
||||
return asset.tags.includes(category)
|
||||
}
|
||||
|
||||
const filterByQuery = (query: string) => (asset: AssetItem) => {
|
||||
if (!query) return true
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const description = getAssetDescription(asset)
|
||||
return (
|
||||
asset.name.toLowerCase().includes(lowerQuery) ||
|
||||
(description && description.toLowerCase().includes(lowerQuery)) ||
|
||||
asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
|
||||
)
|
||||
}
|
||||
|
||||
// Computed filtered and transformed assets
|
||||
const filteredAssets = computed(() => {
|
||||
const filtered = assets
|
||||
.filter(filterByCategory(selectedCategory.value))
|
||||
.filter(filterByQuery(searchQuery.value))
|
||||
|
||||
// Sort assets
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy.value) {
|
||||
case 'date':
|
||||
return (
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
)
|
||||
case 'name':
|
||||
default:
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
})
|
||||
|
||||
// Transform to display format
|
||||
return filtered.map(transformAssetForDisplay)
|
||||
})
|
||||
|
||||
// Actions
|
||||
function selectAsset(asset: AssetDisplayItem): UUID {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Asset selected:', asset.id, asset.name)
|
||||
}
|
||||
return asset.id
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
searchQuery,
|
||||
selectedCategory,
|
||||
sortBy,
|
||||
|
||||
// Computed
|
||||
availableCategories,
|
||||
contentTitle,
|
||||
filteredAssets,
|
||||
|
||||
// Actions
|
||||
selectAsset,
|
||||
transformAssetForDisplay
|
||||
}
|
||||
}
|
||||
203
src/platform/assets/composables/useAssetBrowserDialog.stories.ts
Normal file
203
src/platform/assets/composables/useAssetBrowserDialog.stories.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
|
||||
import { mockAssets } from '@/platform/assets/fixtures/ui-mock-assets'
|
||||
|
||||
// Component that simulates the useAssetBrowserDialog functionality with working close
|
||||
const DialogDemoComponent = {
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const isDialogOpen = ref(false)
|
||||
const currentNodeType = ref('CheckpointLoaderSimple')
|
||||
const currentInputName = ref('ckpt_name')
|
||||
const currentValue = ref('')
|
||||
|
||||
const handleOpenDialog = (
|
||||
nodeType: string,
|
||||
inputName: string,
|
||||
value = ''
|
||||
) => {
|
||||
currentNodeType.value = nodeType
|
||||
currentInputName.value = inputName
|
||||
currentValue.value = value
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
isDialogOpen.value = false
|
||||
}
|
||||
|
||||
const handleAssetSelected = (assetPath: string) => {
|
||||
console.log('Asset selected:', assetPath)
|
||||
alert(`Selected asset: ${assetPath}`)
|
||||
isDialogOpen.value = false // Auto-close like the real composable
|
||||
}
|
||||
|
||||
const handleOpenWithCurrentValue = () => {
|
||||
handleOpenDialog(
|
||||
'CheckpointLoaderSimple',
|
||||
'ckpt_name',
|
||||
'realistic_vision_v5.safetensors'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
isDialogOpen,
|
||||
currentNodeType,
|
||||
currentInputName,
|
||||
currentValue,
|
||||
handleOpenDialog,
|
||||
handleOpenWithCurrentValue,
|
||||
handleCloseDialog,
|
||||
handleAssetSelected,
|
||||
mockAssets
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="relative">
|
||||
<div class="p-8 space-y-4">
|
||||
<h2 class="text-2xl font-bold mb-6">Asset Browser Dialog Demo</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">Different Node Types</h3>
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<button
|
||||
@click="handleOpenDialog('CheckpointLoaderSimple', 'ckpt_name')"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Browse Checkpoints
|
||||
</button>
|
||||
<button
|
||||
@click="handleOpenDialog('VAELoader', 'vae_name')"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||
>
|
||||
Browse VAE
|
||||
</button>
|
||||
<button
|
||||
@click="handleOpenDialog('ControlNetLoader', 'control_net_name')"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
|
||||
>
|
||||
Browse ControlNet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">With Current Value</h3>
|
||||
<button
|
||||
@click="handleOpenWithCurrentValue"
|
||||
class="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700"
|
||||
>
|
||||
Change Current Model
|
||||
</button>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
Opens with "realistic_vision_v5.safetensors" as current value
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-gray-100 rounded">
|
||||
<h4 class="font-semibold mb-2">Instructions:</h4>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li>• Click any button to open the Asset Browser dialog</li>
|
||||
<li>• Select an asset to see the callback in action</li>
|
||||
<li>• Check the browser console for logged events</li>
|
||||
<li>• Try toggling the left panel with different asset types</li>
|
||||
<li>• Close button will work properly in this demo</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Modal Overlay -->
|
||||
<div
|
||||
v-if="isDialogOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
||||
@click.self="handleCloseDialog"
|
||||
>
|
||||
<div class="w-[80vw] h-[80vh] max-w-[80vw] max-h-[80vh] rounded-2xl overflow-hidden">
|
||||
<AssetBrowserModal
|
||||
:assets="mockAssets"
|
||||
:node-type="currentNodeType"
|
||||
:input-name="currentInputName"
|
||||
:current-value="currentValue"
|
||||
@asset-select="handleAssetSelected"
|
||||
@close="handleCloseDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Platform/Assets/useAssetBrowserDialog',
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Demonstrates the AssetBrowserModal functionality as used by the useAssetBrowserDialog composable.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Demo: Story = {
|
||||
render: () => ({
|
||||
components: { DialogDemoComponent },
|
||||
template: `
|
||||
<div>
|
||||
<DialogDemoComponent />
|
||||
|
||||
<!-- Code Example Section -->
|
||||
<div class="p-8 border-t border-gray-200 bg-gray-50">
|
||||
<h2 class="text-2xl font-bold mb-4">Code Example</h2>
|
||||
<p class="text-gray-600 mb-4">
|
||||
This is how you would use the composable in your component:
|
||||
</p>
|
||||
<div class="bg-white p-4 rounded-lg border shadow-sm">
|
||||
<pre><code class="text-sm">import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
const openBrowser = () => {
|
||||
assetBrowserDialog.show({
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
inputName: 'ckpt_name',
|
||||
currentValue: '',
|
||||
onAssetSelected: (assetPath) => {
|
||||
console.log('Selected:', assetPath)
|
||||
// Update your component state
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { openBrowser }
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>💡 Try it:</strong> Use the interactive buttons above to see this code in action!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Complete demo showing both interactive functionality and code examples for using useAssetBrowserDialog to open the Asset Browser modal programmatically.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/platform/assets/composables/useAssetBrowserDialog.ts
Normal file
66
src/platform/assets/composables/useAssetBrowserDialog.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
interface AssetBrowserDialogProps {
|
||||
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
|
||||
nodeType: string
|
||||
/** Widget input name (e.g., 'ckpt_name') */
|
||||
inputName: string
|
||||
/** Current selected asset value */
|
||||
currentValue?: string
|
||||
/** Callback for when an asset is selected */
|
||||
onAssetSelected?: (assetPath: string) => void
|
||||
}
|
||||
|
||||
export const useAssetBrowserDialog = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogKey = 'global-asset-browser'
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: dialogKey })
|
||||
}
|
||||
|
||||
function show(props: AssetBrowserDialogProps) {
|
||||
const handleAssetSelected = (assetPath: string) => {
|
||||
props.onAssetSelected?.(assetPath)
|
||||
hide() // Auto-close on selection
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
// Default dialog configuration for AssetBrowserModal
|
||||
const dialogComponentProps = {
|
||||
headless: true,
|
||||
modal: true,
|
||||
closable: false,
|
||||
pt: {
|
||||
root: {
|
||||
class: 'rounded-2xl overflow-hidden'
|
||||
},
|
||||
header: {
|
||||
class: 'p-0 hidden'
|
||||
},
|
||||
content: {
|
||||
class: 'p-0 m-0 h-full w-full'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: dialogKey,
|
||||
component: AssetBrowserModal,
|
||||
props: {
|
||||
nodeType: props.nodeType,
|
||||
inputName: props.inputName,
|
||||
currentValue: props.currentValue,
|
||||
onSelect: handleAssetSelected,
|
||||
onClose: handleClose
|
||||
},
|
||||
dialogComponentProps
|
||||
})
|
||||
}
|
||||
|
||||
return { show, hide }
|
||||
}
|
||||
56
src/platform/assets/composables/useAssetFilterOptions.ts
Normal file
56
src/platform/assets/composables/useAssetFilterOptions.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { uniqWith } from 'es-toolkit'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SelectOption } from '@/components/input/types'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
/**
|
||||
* Composable that extracts available filter options from asset data
|
||||
* Provides reactive computed properties for file formats and base models
|
||||
*/
|
||||
export function useAssetFilterOptions(assets: AssetItem[] = []) {
|
||||
/**
|
||||
* Extract unique file formats from asset names
|
||||
* Returns sorted SelectOption array with extensions
|
||||
*/
|
||||
const availableFileFormats = computed<SelectOption[]>(() => {
|
||||
const extensions = assets
|
||||
.map((asset) => {
|
||||
const extension = asset.name.split('.').pop()
|
||||
return extension && extension !== asset.name ? extension : null
|
||||
})
|
||||
.filter((extension): extension is string => extension !== null)
|
||||
|
||||
const uniqueExtensions = uniqWith(extensions, (a, b) => a === b)
|
||||
|
||||
return uniqueExtensions.sort().map((format) => ({
|
||||
name: `.${format}`,
|
||||
value: format
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* Extract unique base models from asset user metadata
|
||||
* Returns sorted SelectOption array with base model names
|
||||
*/
|
||||
const availableBaseModels = computed<SelectOption[]>(() => {
|
||||
const models = assets
|
||||
.map((asset) => asset.user_metadata?.base_model)
|
||||
.filter(
|
||||
(baseModel): baseModel is string =>
|
||||
baseModel !== undefined && typeof baseModel === 'string'
|
||||
)
|
||||
|
||||
const uniqueModels = uniqWith(models, (a, b) => a === b)
|
||||
|
||||
return uniqueModels.sort().map((model) => ({
|
||||
name: model,
|
||||
value: model
|
||||
}))
|
||||
})
|
||||
|
||||
return {
|
||||
availableFileFormats,
|
||||
availableBaseModels
|
||||
}
|
||||
}
|
||||
128
src/platform/assets/fixtures/ui-mock-assets.ts
Normal file
128
src/platform/assets/fixtures/ui-mock-assets.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
// 🎭 OBVIOUSLY FAKE MOCK DATA - DO NOT USE IN PRODUCTION! 🎭
|
||||
const fakeFunnyModelNames = [
|
||||
'🎯_totally_real_model_v420.69',
|
||||
'🚀_definitely_not_fake_v999',
|
||||
'🎪_super_legit_checkpoint_pro_max',
|
||||
'🦄_unicorn_dreams_totally_real.model',
|
||||
'🍕_pizza_generator_supreme',
|
||||
'🎸_rock_star_fake_data_v1337',
|
||||
'🌮_taco_tuesday_model_deluxe',
|
||||
'🦖_dino_nugget_generator_v3',
|
||||
'🎮_gamer_fuel_checkpoint_xl',
|
||||
'🍄_mushroom_kingdom_diffusion',
|
||||
'🏴☠️_pirate_treasure_model_arr',
|
||||
'🦋_butterfly_effect_generator',
|
||||
'🎺_jazz_hands_checkpoint_pro',
|
||||
'🥨_pretzel_logic_model_v2',
|
||||
'🌙_midnight_snack_generator',
|
||||
'🎭_drama_llama_checkpoint',
|
||||
'🧙♀️_wizard_hat_diffusion_xl',
|
||||
'🎪_circus_peanut_model_v4',
|
||||
'🦒_giraffe_neck_generator',
|
||||
'🎲_random_stuff_checkpoint_max'
|
||||
]
|
||||
|
||||
const obviouslyFakeDescriptions = [
|
||||
'⚠️ FAKE DATA: Generates 100% authentic fake images with premium mock quality',
|
||||
'🎭 MOCK ALERT: This totally real model creates absolutely genuine fake content',
|
||||
'🚨 NOT REAL: Professional-grade fake imagery for your mock data needs',
|
||||
'🎪 DEMO ONLY: Circus-quality fake generation with extra mock seasoning',
|
||||
'🍕 FAKE FOOD: Generates delicious fake pizzas (not edible in reality)',
|
||||
"🎸 MOCK ROCK: Creates fake rock stars who definitely don't exist",
|
||||
'🌮 TACO FAKERY: Tuesday-themed fake tacos for your mock appetite',
|
||||
'🦖 PREHISTORIC FAKE: Generates extinct fake dinosaurs for demo purposes',
|
||||
'🎮 FAKE GAMING: Level up your mock data with obviously fake content',
|
||||
'🍄 FUNGI FICTION: Magical fake mushrooms from the demo dimension',
|
||||
'🏴☠️ FAKE TREASURE: Arr! This be mock data for ye demo needs, matey!',
|
||||
'🦋 DEMO EFFECT: Small fake changes create big mock differences',
|
||||
'🎺 JAZZ FAKERY: Smooth fake jazz for your mock listening pleasure',
|
||||
'🥨 MOCK LOGIC: Twisted fake reasoning for your demo requirements',
|
||||
'🌙 MIDNIGHT MOCK: Late-night fake snacks for your demo hunger',
|
||||
'🎭 FAKE DRAMA: Over-the-top mock emotions for demo entertainment',
|
||||
'🧙♀️ WIZARD MOCK: Magically fake spells cast with demo ingredients',
|
||||
'🎪 CIRCUS FAKE: Big top mock entertainment under the demo tent',
|
||||
'🦒 TALL FAKE: Reaches new heights of obviously fake content',
|
||||
'🎲 RANDOM MOCK: Generates random fake stuff for your demo pleasure'
|
||||
]
|
||||
|
||||
// API-compliant tag structure: first tag must be root (models/input/output), second is category
|
||||
const modelCategories = ['checkpoints', 'loras', 'embeddings', 'vae']
|
||||
const baseModels = ['sd15', 'sdxl', 'sd35']
|
||||
const fileExtensions = ['.safetensors', '.ckpt', '.pt']
|
||||
const mimeTypes = [
|
||||
'application/octet-stream',
|
||||
'application/x-pytorch',
|
||||
'application/x-safetensors'
|
||||
]
|
||||
|
||||
function getRandomElement<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
function getRandomNumber(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
function getRandomISODate(): string {
|
||||
const start = new Date('2024-01-01').getTime()
|
||||
const end = new Date('2024-12-31').getTime()
|
||||
const randomTime = start + Math.random() * (end - start)
|
||||
return new Date(randomTime).toISOString()
|
||||
}
|
||||
|
||||
function generateFakeAssetHash(): string {
|
||||
const chars = '0123456789abcdef'
|
||||
let hash = 'blake3:'
|
||||
for (let i = 0; i < 64; i++) {
|
||||
hash += chars[Math.floor(Math.random() * chars.length)]
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// 🎭 CREATES OBVIOUSLY FAKE ASSETS FOR DEMO/TEST PURPOSES ONLY! 🎭
|
||||
export function createMockAssets(count: number = 20): AssetItem[] {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const category = getRandomElement(modelCategories)
|
||||
const baseModel = getRandomElement(baseModels)
|
||||
const extension = getRandomElement(fileExtensions)
|
||||
const mimeType = getRandomElement(mimeTypes)
|
||||
const sizeInBytes = getRandomNumber(
|
||||
500 * 1024 * 1024,
|
||||
8 * 1024 * 1024 * 1024
|
||||
) // 500MB to 8GB
|
||||
const createdAt = getRandomISODate()
|
||||
const updatedAt = createdAt
|
||||
const lastAccessTime = getRandomISODate()
|
||||
|
||||
const fakeFileName = `${fakeFunnyModelNames[index]}${extension}`
|
||||
|
||||
return {
|
||||
id: `mock-asset-uuid-${(index + 1).toString().padStart(3, '0')}-fake`,
|
||||
name: fakeFileName,
|
||||
asset_hash: generateFakeAssetHash(),
|
||||
size: sizeInBytes,
|
||||
mime_type: mimeType,
|
||||
tags: [
|
||||
'models', // Root tag (required first)
|
||||
category, // Category tag (required second for models)
|
||||
'fake-data', // Obviously fake tag
|
||||
...(Math.random() > 0.5 ? ['demo-mode'] : ['test-only']),
|
||||
...(Math.random() > 0.7 ? ['obviously-mock'] : [])
|
||||
],
|
||||
preview_url: `/api/assets/mock-asset-uuid-${(index + 1).toString().padStart(3, '0')}-fake/content`,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
last_access_time: lastAccessTime,
|
||||
user_metadata: {
|
||||
description: obviouslyFakeDescriptions[index],
|
||||
base_model: baseModel,
|
||||
original_name: fakeFunnyModelNames[index],
|
||||
warning: '🚨 THIS IS FAKE DEMO DATA - NOT A REAL MODEL! 🚨'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const mockAssets = createMockAssets(20)
|
||||
@@ -1,12 +1,19 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// Zod schemas for asset API validation
|
||||
// Zod schemas for asset API validation matching ComfyUI Assets REST API spec
|
||||
const zAsset = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
asset_hash: z.string(),
|
||||
size: z.number(),
|
||||
created_at: z.string().optional()
|
||||
mime_type: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
preview_url: z.string().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
last_access_time: z.string(),
|
||||
user_metadata: z.record(z.unknown()).optional(), // API allows arbitrary key-value pairs
|
||||
preview_id: z.string().nullable().optional()
|
||||
})
|
||||
|
||||
const zAssetResponse = z.object({
|
||||
@@ -20,19 +27,22 @@ const zModelFolder = z.object({
|
||||
folders: z.array(z.string())
|
||||
})
|
||||
|
||||
// Zod schema for ModelFile to align with interface
|
||||
const zModelFile = z.object({
|
||||
name: z.string(),
|
||||
pathIndex: z.number()
|
||||
})
|
||||
|
||||
// Export schemas following repository patterns
|
||||
export const assetResponseSchema = zAssetResponse
|
||||
|
||||
// Export types derived from Zod schemas
|
||||
export type AssetItem = z.infer<typeof zAsset>
|
||||
export type AssetResponse = z.infer<typeof zAssetResponse>
|
||||
export type ModelFolder = z.infer<typeof zModelFolder>
|
||||
export type ModelFile = z.infer<typeof zModelFile>
|
||||
|
||||
// Common interfaces for API responses
|
||||
export interface ModelFile {
|
||||
name: string
|
||||
pathIndex: number
|
||||
}
|
||||
|
||||
// Legacy interface for backward compatibility (now aligned with Zod schema)
|
||||
export interface ModelFolderInfo {
|
||||
name: string
|
||||
folders: string[]
|
||||
|
||||
@@ -67,7 +67,7 @@ function createAssetService() {
|
||||
)
|
||||
|
||||
// Blacklist directories we don't want to show
|
||||
const blacklistedDirectories = ['configs']
|
||||
const blacklistedDirectories = new Set(['configs'])
|
||||
|
||||
// Extract directory names from assets that actually exist, exclude missing assets
|
||||
const discoveredFolders = new Set<string>(
|
||||
@@ -75,7 +75,7 @@ function createAssetService() {
|
||||
?.filter((asset) => !asset.tags.includes(MISSING_TAG))
|
||||
?.flatMap((asset) => asset.tags)
|
||||
?.filter(
|
||||
(tag) => tag !== MODELS_TAG && !blacklistedDirectories.includes(tag)
|
||||
(tag) => tag !== MODELS_TAG && !blacklistedDirectories.has(tag)
|
||||
) ?? []
|
||||
)
|
||||
|
||||
|
||||
27
src/platform/assets/utils/assetMetadataUtils.ts
Normal file
27
src/platform/assets/utils/assetMetadataUtils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
/**
|
||||
* Type-safe utilities for extracting metadata from assets
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safely extracts string description from asset metadata
|
||||
* @param asset - The asset to extract description from
|
||||
* @returns The description string or null if not present/not a string
|
||||
*/
|
||||
export function getAssetDescription(asset: AssetItem): string | null {
|
||||
return typeof asset.user_metadata?.description === 'string'
|
||||
? asset.user_metadata.description
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extracts string base_model from asset metadata
|
||||
* @param asset - The asset to extract base_model from
|
||||
* @returns The base_model string or null if not present/not a string
|
||||
*/
|
||||
export function getAssetBaseModel(asset: AssetItem): string | null {
|
||||
return typeof asset.user_metadata?.base_model === 'string'
|
||||
? asset.user_metadata.base_model
|
||||
: null
|
||||
}
|
||||
@@ -404,7 +404,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'ar', text: 'عربي' }
|
||||
{ value: 'ar', text: 'عربي' },
|
||||
{ value: 'tr', text: 'Türkçe' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import { downloadBlob } from '@/scripts/utils'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt, generateUUID } from '@/utils/formatUtil'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
export const useWorkflowService = () => {
|
||||
const settingStore = useSettingStore()
|
||||
@@ -112,13 +112,6 @@ export const useWorkflowService = () => {
|
||||
await renameWorkflow(workflow, newPath)
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
} else {
|
||||
// Generate new id when saving existing workflow as a new file
|
||||
const id = generateUUID()
|
||||
const state = JSON.parse(
|
||||
JSON.stringify(workflow.activeState)
|
||||
) as ComfyWorkflowJSON
|
||||
state.id = id
|
||||
|
||||
const tempWorkflow = workflowStore.saveAs(workflow, newPath)
|
||||
await openWorkflow(tempWorkflow)
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { generateUUID, getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -320,12 +320,19 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
existingWorkflow: ComfyWorkflow,
|
||||
path: string
|
||||
): ComfyWorkflow => {
|
||||
// Generate new id when saving existing workflow as a new file
|
||||
const id = generateUUID()
|
||||
const state = JSON.parse(
|
||||
JSON.stringify(existingWorkflow.activeState)
|
||||
) as ComfyWorkflowJSON
|
||||
state.id = id
|
||||
|
||||
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
|
||||
path,
|
||||
modified: Date.now(),
|
||||
size: -1
|
||||
})
|
||||
workflow.originalContent = workflow.content = existingWorkflow.content
|
||||
workflow.originalContent = workflow.content = JSON.stringify(state)
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { getActivePinia } from 'pinia'
|
||||
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type {
|
||||
SlotDragSource,
|
||||
SlotDropCandidate
|
||||
} from '@/renderer/core/linkInteractions/slotLinkDragState'
|
||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
interface CompatibilityResult {
|
||||
@@ -15,9 +18,10 @@ interface CompatibilityResult {
|
||||
targetSlot?: INodeInputSlot | INodeOutputSlot
|
||||
}
|
||||
|
||||
function resolveNode(nodeId: string | number) {
|
||||
const canvas = app.canvas
|
||||
const graph = canvas?.graph
|
||||
function resolveNode(nodeId: NodeId) {
|
||||
const pinia = getActivePinia()
|
||||
const canvasStore = pinia ? useCanvasStore() : null
|
||||
const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph
|
||||
if (!graph) return null
|
||||
const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId
|
||||
if (Number.isNaN(id)) return null
|
||||
@@ -28,10 +32,7 @@ export function evaluateCompatibility(
|
||||
source: SlotDragSource,
|
||||
candidate: SlotDropCandidate
|
||||
): CompatibilityResult {
|
||||
if (
|
||||
candidate.layout.nodeId === source.nodeId &&
|
||||
candidate.layout.index === source.slotIndex
|
||||
) {
|
||||
if (candidate.layout.nodeId === source.nodeId) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
@@ -50,20 +51,9 @@ export function evaluateCompatibility(
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
const sourceSlot = isOutputToInput
|
||||
? sourceNode.outputs?.[source.slotIndex]
|
||||
: sourceNode.inputs?.[source.slotIndex]
|
||||
const targetSlot = isOutputToInput
|
||||
? targetNode.inputs?.[candidate.layout.index]
|
||||
: targetNode.outputs?.[candidate.layout.index]
|
||||
|
||||
if (!sourceSlot || !targetSlot) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
if (isOutputToInput) {
|
||||
const outputSlot = sourceSlot as INodeOutputSlot | undefined
|
||||
const inputSlot = targetSlot as INodeInputSlot | undefined
|
||||
const outputSlot = sourceNode.outputs?.[source.slotIndex]
|
||||
const inputSlot = targetNode.inputs?.[candidate.layout.index]
|
||||
if (!outputSlot || !inputSlot) {
|
||||
return { allowable: false }
|
||||
}
|
||||
@@ -72,8 +62,8 @@ export function evaluateCompatibility(
|
||||
return { allowable, targetNode, targetSlot: inputSlot }
|
||||
}
|
||||
|
||||
const inputSlot = sourceSlot as INodeInputSlot | undefined
|
||||
const outputSlot = targetSlot as INodeOutputSlot | undefined
|
||||
const inputSlot = sourceNode.inputs?.[source.slotIndex]
|
||||
const outputSlot = targetNode.outputs?.[candidate.layout.index]
|
||||
if (!inputSlot || !outputSlot) {
|
||||
return { allowable: false }
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { reactive, readonly, shallowReactive } from 'vue'
|
||||
import { reactive, readonly } from 'vue'
|
||||
|
||||
import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { SlotLayout } from '@/renderer/core/layout/types'
|
||||
import type { Point, SlotLayout } from '@/renderer/core/layout/types'
|
||||
|
||||
type SlotDragType = 'input' | 'output'
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface SlotDragSource {
|
||||
slotIndex: number
|
||||
type: SlotDragType
|
||||
direction: LinkDirection
|
||||
position: Readonly<{ x: number; y: number }>
|
||||
position: Readonly<Point>
|
||||
}
|
||||
|
||||
export interface SlotDropCandidate {
|
||||
@@ -21,8 +21,8 @@ export interface SlotDropCandidate {
|
||||
}
|
||||
|
||||
interface PointerPosition {
|
||||
client: Readonly<{ x: number; y: number }>
|
||||
canvas: Readonly<{ x: number; y: number }>
|
||||
client: Point
|
||||
canvas: Point
|
||||
}
|
||||
|
||||
interface SlotDragState {
|
||||
@@ -33,24 +33,27 @@ interface SlotDragState {
|
||||
candidate: SlotDropCandidate | null
|
||||
}
|
||||
|
||||
const defaultPointer: PointerPosition = Object.freeze({
|
||||
client: { x: 0, y: 0 },
|
||||
canvas: { x: 0, y: 0 }
|
||||
})
|
||||
|
||||
const state = reactive<SlotDragState>({
|
||||
active: false,
|
||||
pointerId: null,
|
||||
source: null,
|
||||
pointer: defaultPointer,
|
||||
pointer: {
|
||||
client: { x: 0, y: 0 },
|
||||
canvas: { x: 0, y: 0 }
|
||||
},
|
||||
candidate: null
|
||||
})
|
||||
|
||||
function updatePointerPosition(position: PointerPosition) {
|
||||
state.pointer = shallowReactive({
|
||||
client: position.client,
|
||||
canvas: position.canvas
|
||||
})
|
||||
function updatePointerPosition(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
canvasX: number,
|
||||
canvasY: number
|
||||
) {
|
||||
state.pointer.client.x = clientX
|
||||
state.pointer.client.y = clientY
|
||||
state.pointer.canvas.x = canvasX
|
||||
state.pointer.canvas.y = canvasY
|
||||
}
|
||||
|
||||
function setCandidate(candidate: SlotDropCandidate | null) {
|
||||
@@ -68,7 +71,10 @@ function endDrag() {
|
||||
state.active = false
|
||||
state.pointerId = null
|
||||
state.source = null
|
||||
state.pointer = defaultPointer
|
||||
state.pointer.client.x = 0
|
||||
state.pointer.client.y = 0
|
||||
state.pointer.canvas.x = 0
|
||||
state.pointer.canvas.y = 0
|
||||
state.candidate = null
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import type {
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
|
||||
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
import {
|
||||
type SlotDragSource,
|
||||
useSlotLinkDragState
|
||||
} from '@/renderer/core/linkInteractions/slotLinkDragState'
|
||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
|
||||
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
|
||||
return {
|
||||
@@ -11,12 +11,21 @@ import { app } from '@/scripts/app'
|
||||
*/
|
||||
export function useCanvasInteractions() {
|
||||
const settingStore = useSettingStore()
|
||||
const { getCanvas } = useCanvasStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { getCanvas } = canvasStore
|
||||
|
||||
const isStandardNavMode = computed(
|
||||
() => settingStore.get('Comfy.Canvas.NavigationMode') === 'standard'
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether Vue node components should handle pointer events.
|
||||
* Returns false when canvas is in read-only/panning mode (e.g., space key held for panning).
|
||||
*/
|
||||
const shouldHandleNodePointerEvents = computed(
|
||||
() => !(canvasStore.canvas?.read_only ?? false)
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles wheel events from UI components that should be forwarded to canvas
|
||||
* when appropriate (e.g., Ctrl+wheel for zoom in standard mode)
|
||||
@@ -97,6 +106,7 @@ export function useCanvasInteractions() {
|
||||
return {
|
||||
handleWheel,
|
||||
handlePointer,
|
||||
forwardEventToCanvas
|
||||
forwardEventToCanvas,
|
||||
shouldHandleNodePointerEvents
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* CRDT ensures conflict-free operations for both single and multi-user scenarios.
|
||||
*/
|
||||
import log from 'loglevel'
|
||||
import { type ComputedRef, type Ref, computed, customRef } from 'vue'
|
||||
import { type ComputedRef, type Ref, computed, customRef, ref } from 'vue'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { ACTOR_CONFIG } from '@/renderer/core/layout/constants'
|
||||
@@ -134,6 +134,9 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
private slotSpatialIndex: SpatialIndexManager // For slots
|
||||
private rerouteSpatialIndex: SpatialIndexManager // For reroutes
|
||||
|
||||
// Vue dragging state for selection toolbox (public ref for direct mutation)
|
||||
public isDraggingVueNodes = ref(false)
|
||||
|
||||
constructor() {
|
||||
// Initialize Yjs data structures
|
||||
this.ynodes = this.ydoc.getMap('nodes')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user