mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 23:04:06 +00:00
Merge branch 'main' of https://github.com/Comfy-Org/ComfyUI_frontend into mobile-dvh
This commit is contained in:
11
.cursorrules
11
.cursorrules
@@ -8,6 +8,15 @@ const vue3CompositionApiBestPractices = [
|
||||
"Use watch and watchEffect for side effects",
|
||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||
"Utilize provide/inject for dependency injection",
|
||||
"Use vue 3.5 style of default prop declaration. Example:
|
||||
|
||||
const { nodes, showTotal = true } = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
}>()
|
||||
|
||||
",
|
||||
"Organize vue component in <template> <script> <style> order",
|
||||
]
|
||||
|
||||
// Folder structure
|
||||
@@ -40,4 +49,6 @@ const additionalInstructions = `
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
10. Use vue-i18n in composition API for any string literals. Place new translation
|
||||
entries in src/locales/en/main.json.
|
||||
`;
|
||||
|
||||
25
.vscode/extensions.json
vendored
Normal file
25
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"austenc.tailwind-docs",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"figma.figma-vscode-extension",
|
||||
"github.vscode-github-actions",
|
||||
"github.vscode-pull-request-github",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"lokalise.i18n-ally",
|
||||
"ms-playwright.playwright",
|
||||
"vitest.explorer",
|
||||
"vue.volar",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"deque-systems.vscode-axe-linter",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"donjayamanne.githistory",
|
||||
"wix.vscode-import-cost",
|
||||
"prograhammer.tslint-vue",
|
||||
"antfu.vite"
|
||||
]
|
||||
}
|
||||
1170
package-lock.json
generated
1170
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.16.7",
|
||||
"version": "1.17.0",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.13.1",
|
||||
"@comfyorg/litegraph": "^0.13.3",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -91,6 +91,7 @@
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -103,6 +104,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.3",
|
||||
"vue-router": "^4.4.3",
|
||||
"vuefire": "^3.2.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
}
|
||||
|
||||
BIN
public/assets/images/Comfy_Logo_x32.png
Normal file
BIN
public/assets/images/Comfy_Logo_x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex justify-between text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.name"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
node.name
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-base font-medium leading-tight">
|
||||
{{ node.cost.toFixed(costPrecision) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<template v-if="showTotal && nodes.length > 1">
|
||||
<Divider class="my-2" />
|
||||
<div class="flex justify-between items-center border-t px-3">
|
||||
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-yellow-500 p-1"
|
||||
/>
|
||||
<span>{{ totalCost.toFixed(costPrecision) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
nodes,
|
||||
showTotal = true,
|
||||
costPrecision = 3
|
||||
} = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
costPrecision?: number
|
||||
}>()
|
||||
|
||||
const totalCost = computed(() =>
|
||||
nodes.reduce((sum, node) => sum + node.cost, 0)
|
||||
)
|
||||
</script>
|
||||
43
src/components/dialog/content/ApiNodesSignInContent.vue
Normal file
43
src/components/dialog/content/ApiNodesSignInContent.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- Prompt user that the workflow contains API nodes that needs login to run -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 max-w-96 h-110 p-2">
|
||||
<div class="text-2xl font-medium mb-2">
|
||||
{{ t('apiNodesSignInDialog.title') }}
|
||||
</div>
|
||||
|
||||
<div class="text-base mb-4">
|
||||
{{ t('apiNodesSignInDialog.message') }}
|
||||
</div>
|
||||
|
||||
<ApiNodesCostBreakdown :nodes="apiNodes" :show-total="true" />
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<Button :label="t('g.learnMore')" link />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
:label="t('g.cancel')"
|
||||
outlined
|
||||
severity="secondary"
|
||||
@click="onCancel?.()"
|
||||
/>
|
||||
<Button :label="t('g.login')" @click="onLogin?.()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ApiNodesCostBreakdown from '@/components/common/ApiNodesCostBreakdown.vue'
|
||||
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { apiNodes, onLogin, onCancel } = defineProps<{
|
||||
apiNodes: ApiNodeCost[]
|
||||
onLogin?: () => void
|
||||
onCancel?: () => void
|
||||
}>()
|
||||
</script>
|
||||
124
src/components/dialog/content/SignInContent.vue
Normal file
124
src/components/dialog/content/SignInContent.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="w-96 p-2">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<SignUpForm v-else @submit="signInWithEmail" />
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Terms -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<span class="text-blue-500 cursor-pointer">{{
|
||||
t('auth.login.termsLink')
|
||||
}}</span>
|
||||
{{ t('auth.login.andText') }}
|
||||
<span class="text-blue-500 cursor-pointer">{{
|
||||
t('auth.login.privacyLink')
|
||||
}}</span
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
import SignInForm from './signin/SignInForm.vue'
|
||||
import SignUpForm from './signin/SignUpForm.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
onSuccess: () => void
|
||||
}>()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const isSignIn = ref(true)
|
||||
const toggleState = () => {
|
||||
isSignIn.value = !isSignIn.value
|
||||
}
|
||||
|
||||
const signInWithGoogle = wrapWithErrorHandlingAsync(async () => {
|
||||
await firebaseAuthStore.loginWithGoogle()
|
||||
onSuccess()
|
||||
})
|
||||
|
||||
const signInWithGithub = wrapWithErrorHandlingAsync(async () => {
|
||||
await firebaseAuthStore.loginWithGithub()
|
||||
onSuccess()
|
||||
})
|
||||
|
||||
const signInWithEmail = wrapWithErrorHandlingAsync(
|
||||
async (values: SignInData | SignUpData) => {
|
||||
const { email, password } = values
|
||||
if (isSignIn.value) {
|
||||
await firebaseAuthStore.login(email, password)
|
||||
} else {
|
||||
await firebaseAuthStore.register(email, password)
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -101,7 +101,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
|
||||
89
src/components/dialog/content/signin/SignInForm.vue
Normal file
89
src/components/dialog/content/signin/SignInForm.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signInSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-in-email"
|
||||
>
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
pt:root:id="comfy-org-sign-in-email"
|
||||
pt:root:autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.login.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium"
|
||||
for="comfy-org-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span class="text-muted text-base font-medium cursor-pointer">
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</span>
|
||||
</div>
|
||||
<Password
|
||||
input-id="comfy-org-sign-in-password"
|
||||
pt:pc-input-text:root:autocomplete="current-password"
|
||||
name="password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.password?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.password?.invalid" class="text-red-500">{{
|
||||
$form.password.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignInData]
|
||||
}>()
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
165
src/components/dialog/content/signin/SignUpForm.vue
Normal file
165
src/components/dialog/content/signin/SignUpForm.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signUpSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-up-email"
|
||||
>
|
||||
{{ t('auth.signup.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
pt:root:id="comfy-org-sign-up-email"
|
||||
pt:root:autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.signup.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium"
|
||||
for="comfy-org-sign-up-password"
|
||||
>
|
||||
{{ t('auth.signup.passwordLabel') }}
|
||||
</label>
|
||||
</div>
|
||||
<Password
|
||||
v-model="password"
|
||||
input-id="comfy-org-sign-up-password"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
name="password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.signup.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.password?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<small
|
||||
v-if="$form.password?.dirty || $form.password?.invalid"
|
||||
class="text-sm"
|
||||
>
|
||||
{{ t('validation.password.requirements') }}:
|
||||
<ul class="mt-1 space-y-1">
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.length
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.minLength') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.uppercase
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.uppercase') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.lowercase
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.lowercase') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.number
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.number') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.special
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.special') }}
|
||||
</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-up-confirm-password"
|
||||
>
|
||||
{{ t('auth.login.confirmPasswordLabel') }}
|
||||
</label>
|
||||
<Password
|
||||
name="confirmPassword"
|
||||
input-id="comfy-org-sign-up-confirm-password"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.confirmPasswordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.confirmPassword?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.confirmPassword?.error" class="text-red-500">{{
|
||||
$form.confirmPassword.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('auth.signup.signUpButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
const password = ref('')
|
||||
|
||||
// TODO: Use dynamic form to better organize the password checks.
|
||||
// Ref: https://primevue.org/forms/#dynamic
|
||||
const passwordChecks = computed(() => ({
|
||||
length: password.value.length >= 8 && password.value.length <= 32,
|
||||
uppercase: /[A-Z]/.test(password.value),
|
||||
lowercase: /[a-z]/.test(password.value),
|
||||
number: /\d/.test(password.value),
|
||||
special: /[^A-Za-z0-9]/.test(password.value)
|
||||
}))
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignUpData]
|
||||
}>()
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignUpData)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
6
src/components/dialog/header/ComfyOrgHeader.vue
Normal file
6
src/components/dialog/header/ComfyOrgHeader.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- A dialog header with ComfyOrg logo -->
|
||||
<template>
|
||||
<div class="px-2 py-4">
|
||||
<img src="/assets/images/Comfy_Logo_x32.png" alt="ComfyOrg Logo" />
|
||||
</div>
|
||||
</template>
|
||||
12
src/config/firebase.ts
Normal file
12
src/config/firebase.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FirebaseOptions } from 'firebase/app'
|
||||
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = {
|
||||
apiKey: 'AIzaSyC2-fomLqgCjb7ELwta1I9cEarPK8ziTGs',
|
||||
authDomain: 'dreamboothy.firebaseapp.com',
|
||||
databaseURL: 'https://dreamboothy-default-rtdb.firebaseio.com',
|
||||
projectId: 'dreamboothy',
|
||||
storageBucket: 'dreamboothy.appspot.com',
|
||||
messagingSenderId: '357148958219',
|
||||
appId: '1:357148958219:web:f5917f72e5f36a2015310e',
|
||||
measurementId: 'G-3ZBD3MBTG4'
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location fo the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.QUESTION,
|
||||
|
||||
@@ -18,7 +18,39 @@ import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Load3D',
|
||||
|
||||
settings: [
|
||||
{
|
||||
id: 'Comfy.Load3D.ShowGrid',
|
||||
category: ['3D', 'Scene', 'Initial Grid Visibility'],
|
||||
name: 'Initial Grid Visibility',
|
||||
tooltip:
|
||||
'Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.ShowPreview',
|
||||
category: ['3D', 'Scene', 'Initial Preview Visibility'],
|
||||
name: 'Initial Preview Visibility',
|
||||
tooltip:
|
||||
'Controls whether the preview screen is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.CameraType',
|
||||
category: ['3D', 'Camera', 'Initial Camera Type'],
|
||||
name: 'Initial Camera Type',
|
||||
tooltip:
|
||||
'Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.',
|
||||
type: 'combo',
|
||||
options: ['perspective', 'orthographic'],
|
||||
defaultValue: 'perspective',
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
LOAD_3D(node) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IWidget } from '@comfyorg/litegraph'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
class Load3DConfiguration {
|
||||
constructor(private load3d: Load3d) {}
|
||||
@@ -72,15 +73,21 @@ class Load3DConfiguration {
|
||||
private setupDefaultProperties() {
|
||||
const cameraType = this.load3d.loadNodeProperty(
|
||||
'Camera Type',
|
||||
'perspective'
|
||||
useSettingStore().get('Comfy.Load3D.CameraType')
|
||||
)
|
||||
this.load3d.toggleCamera(cameraType)
|
||||
|
||||
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
|
||||
const showGrid = this.load3d.loadNodeProperty(
|
||||
'Show Grid',
|
||||
useSettingStore().get('Comfy.Load3D.ShowGrid')
|
||||
)
|
||||
|
||||
this.load3d.toggleGrid(showGrid)
|
||||
|
||||
const showPreview = this.load3d.loadNodeProperty('Show Preview', true)
|
||||
const showPreview = this.load3d.loadNodeProperty(
|
||||
'Show Preview',
|
||||
useSettingStore().get('Comfy.Load3D.ShowPreview')
|
||||
)
|
||||
|
||||
this.load3d.togglePreview(showPreview)
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@
|
||||
"disabling": "Disabling",
|
||||
"updating": "Updating",
|
||||
"migrate": "Migrate",
|
||||
"updateAvailable": "Update Available"
|
||||
"updateAvailable": "Update Available",
|
||||
"login": "Login",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -716,7 +718,11 @@
|
||||
"CustomColorPalettes": "Custom Color Palettes",
|
||||
"UV": "UV",
|
||||
"ContextMenu": "Context Menu",
|
||||
"Reroute": "Reroute"
|
||||
"Reroute": "Reroute",
|
||||
"Load 3D": "Load 3D",
|
||||
"Camera": "Camera",
|
||||
"Scene": "Scene",
|
||||
"3D": "3D"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -972,6 +978,15 @@
|
||||
"extensionFileHint": "This may be due to the following script",
|
||||
"promptExecutionError": "Prompt execution failed"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"title": "Sign In Required to Use API Nodes",
|
||||
"message": "This workflow contains API Nodes, which require you to be signed in to your account in order to run."
|
||||
},
|
||||
"apiNodesCostBreakdown": {
|
||||
"title": "API Node(s)",
|
||||
"costPerRun": "Cost per run",
|
||||
"totalCost": "Total Cost"
|
||||
},
|
||||
"desktopUpdate": {
|
||||
"title": "Updating ComfyUI Desktop",
|
||||
"description": "ComfyUI Desktop is installing new dependencies. This may take a few minutes.",
|
||||
@@ -1036,5 +1051,56 @@
|
||||
"noTemplatesToExport": "No templates to export",
|
||||
"failedToFetchLogs": "Failed to fetch server logs",
|
||||
"migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute."
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Log in to your account",
|
||||
"newUser": "New here?",
|
||||
"signUp": "Sign up",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"confirmPasswordPlaceholder": "Enter the same password again",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"loginButton": "Log in",
|
||||
"orContinueWith": "Or continue with",
|
||||
"loginWithGoogle": "Log in with Google",
|
||||
"loginWithGithub": "Log in with Github",
|
||||
"termsText": "By clicking \"Next\" or \"Sign Up\", you agree to our",
|
||||
"termsLink": "Terms of Use",
|
||||
"andText": "and",
|
||||
"privacyLink": "Privacy Policy",
|
||||
"success": "Login successful",
|
||||
"failed": "Login failed"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
"alreadyHaveAccount": "Already have an account?",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter new password",
|
||||
"signUpButton": "Sign up",
|
||||
"signIn": "Sign in",
|
||||
"signUpWithGoogle": "Sign up with Google",
|
||||
"signUpWithGithub": "Sign up with Github"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Invalid email address",
|
||||
"required": "Required",
|
||||
"minLength": "Must be at least {length} characters",
|
||||
"maxLength": "Must be no more than {length} characters",
|
||||
"password": {
|
||||
"requirements": "Password requirements",
|
||||
"minLength": "Must be between 8 and 32 characters",
|
||||
"uppercase": "Must contain at least one uppercase letter",
|
||||
"lowercase": "Must contain at least one lowercase letter",
|
||||
"number": "Must contain at least one number",
|
||||
"special": "Must contain at least one special character",
|
||||
"match": "Passwords must match"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,22 @@
|
||||
"Hidden": "Hidden"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Initial Camera Type",
|
||||
"tooltip": "Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.",
|
||||
"options": {
|
||||
"perspective": "perspective",
|
||||
"orthographic": "orthographic"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Initial Grid Visibility",
|
||||
"tooltip": "Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation."
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Initial Preview Visibility",
|
||||
"tooltip": "Controls whether the preview screen is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation."
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Language"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "Costo por ejecución",
|
||||
"title": "Nodo(s) de API",
|
||||
"totalCost": "Costo total"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "Este flujo de trabajo contiene nodos de API, que requieren que inicies sesión en tu cuenta para poder ejecutar.",
|
||||
"title": "Se requiere iniciar sesión para usar los nodos de API"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "y",
|
||||
"confirmPasswordLabel": "Confirmar contraseña",
|
||||
"confirmPasswordPlaceholder": "Ingresa la misma contraseña nuevamente",
|
||||
"emailLabel": "Correo electrónico",
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"failed": "Inicio de sesión fallido",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"loginWithGithub": "Iniciar sesión con Github",
|
||||
"loginWithGoogle": "Iniciar sesión con Google",
|
||||
"newUser": "¿Eres nuevo aquí?",
|
||||
"orContinueWith": "O continuar con",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa tu contraseña",
|
||||
"privacyLink": "Política de privacidad",
|
||||
"signUp": "Regístrate",
|
||||
"success": "Inicio de sesión exitoso",
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||
"emailLabel": "Correo electrónico",
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa una nueva contraseña",
|
||||
"signIn": "Iniciar sesión",
|
||||
"signUpButton": "Registrarse",
|
||||
"signUpWithGithub": "Registrarse con Github",
|
||||
"signUpWithGoogle": "Registrarse con Google",
|
||||
"title": "Crea una cuenta"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Error al copiar al portapapeles",
|
||||
"errorNotSupported": "API del portapapeles no soportada en su navegador",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "Instalando",
|
||||
"interrupted": "Interrumpido",
|
||||
"keybinding": "Combinación de teclas",
|
||||
"learnMore": "Aprende más",
|
||||
"loadAllFolders": "Cargar todas las carpetas",
|
||||
"loadWorkflow": "Cargar flujo de trabajo",
|
||||
"loading": "Cargando",
|
||||
"login": "Iniciar sesión",
|
||||
"logs": "Registros",
|
||||
"migrate": "Migrar",
|
||||
"missing": "Faltante",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "Solucionar problemas"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "Acerca de",
|
||||
"Appearance": "Apariencia",
|
||||
"BrushAdjustment": "Ajuste de Pincel",
|
||||
"Camera": "Cámara",
|
||||
"Canvas": "Lienzo",
|
||||
"ColorPalette": "Paleta de Colores",
|
||||
"Comfy": "Comfy",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "Enlace",
|
||||
"LinkRelease": "Liberación de Enlace",
|
||||
"LiteGraph": "Lite Graph",
|
||||
"Load 3D": "Cargar 3D",
|
||||
"Locale": "Localización",
|
||||
"Mask Editor": "Editor de Máscara",
|
||||
"Menu": "Menú",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "Botón de Cola",
|
||||
"Reroute": "Reenrutar",
|
||||
"RerouteBeta": "Reroute Beta",
|
||||
"Scene": "Escena",
|
||||
"Server": "Servidor",
|
||||
"Server-Config": "Configuración del Servidor",
|
||||
"Settings": "Configuraciones",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "Siguiente",
|
||||
"selectUser": "Selecciona un usuario"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Dirección de correo electrónico inválida",
|
||||
"maxLength": "No debe tener más de {length} caracteres",
|
||||
"minLength": "Debe tener al menos {length} caracteres",
|
||||
"password": {
|
||||
"lowercase": "Debe contener al menos una letra minúscula",
|
||||
"match": "Las contraseñas deben coincidir",
|
||||
"minLength": "Debe tener entre 8 y 32 caracteres",
|
||||
"number": "Debe contener al menos un número",
|
||||
"requirements": "Requisitos de la contraseña",
|
||||
"special": "Debe contener al menos un carácter especial",
|
||||
"uppercase": "Debe contener al menos una letra mayúscula"
|
||||
},
|
||||
"required": "Requerido"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Empezar",
|
||||
"title": "Bienvenido a ComfyUI"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "Recto"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Tipo de Cámara",
|
||||
"options": {
|
||||
"orthographic": "ortográfica",
|
||||
"perspective": "perspectiva"
|
||||
},
|
||||
"tooltip": "Controla si la cámara es perspectiva u ortográfica por defecto cuando se crea un nuevo widget 3D. Este valor predeterminado aún puede ser alternado individualmente para cada widget después de su creación."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Mostrar Cuadrícula",
|
||||
"tooltip": "Cambiar para mostrar cuadrícula por defecto"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Mostrar Previsualización",
|
||||
"tooltip": "Cambiar para mostrar previsualización por defecto"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Idioma"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "Coût par exécution",
|
||||
"title": "Nœud(s) API",
|
||||
"totalCost": "Coût total"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "Ce flux de travail contient des nœuds API, qui nécessitent que vous soyez connecté à votre compte pour pouvoir fonctionner.",
|
||||
"title": "Connexion requise pour utiliser les nœuds API"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "et",
|
||||
"confirmPasswordLabel": "Confirmer le mot de passe",
|
||||
"confirmPasswordPlaceholder": "Entrez à nouveau le même mot de passe",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"failed": "Échec de la connexion",
|
||||
"forgotPassword": "Mot de passe oublié?",
|
||||
"loginButton": "Se connecter",
|
||||
"loginWithGithub": "Se connecter avec Github",
|
||||
"loginWithGoogle": "Se connecter avec Google",
|
||||
"newUser": "Nouveau ici?",
|
||||
"orContinueWith": "Ou continuer avec",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez votre mot de passe",
|
||||
"privacyLink": "Politique de confidentialité",
|
||||
"signUp": "S'inscrire",
|
||||
"success": "Connexion réussie",
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Vous avez déjà un compte?",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez un nouveau mot de passe",
|
||||
"signIn": "Se connecter",
|
||||
"signUpButton": "S'inscrire",
|
||||
"signUpWithGithub": "S'inscrire avec Github",
|
||||
"signUpWithGoogle": "S'inscrire avec Google",
|
||||
"title": "Créer un compte"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Échec de la copie dans le presse-papiers",
|
||||
"errorNotSupported": "L'API du presse-papiers n'est pas prise en charge par votre navigateur",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "Installation",
|
||||
"interrupted": "Interrompu",
|
||||
"keybinding": "Raccourci clavier",
|
||||
"learnMore": "En savoir plus",
|
||||
"loadAllFolders": "Charger tous les dossiers",
|
||||
"loadWorkflow": "Charger le flux de travail",
|
||||
"loading": "Chargement",
|
||||
"login": "Connexion",
|
||||
"logs": "Journaux",
|
||||
"migrate": "Migrer",
|
||||
"missing": "Manquant",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "Dépannage"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "À Propos",
|
||||
"Appearance": "Apparence",
|
||||
"BrushAdjustment": "Ajustement de Brosse",
|
||||
"Camera": "Caméra",
|
||||
"Canvas": "Toile",
|
||||
"ColorPalette": "Palette de Couleurs",
|
||||
"Comfy": "Confort",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "Lien",
|
||||
"LinkRelease": "Libération de Lien",
|
||||
"LiteGraph": "Lite Graph",
|
||||
"Load 3D": "Charger 3D",
|
||||
"Locale": "Locale",
|
||||
"Mask Editor": "Éditeur de Masque",
|
||||
"Menu": "Menu",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "Bouton de File d'Attente",
|
||||
"Reroute": "Réacheminement",
|
||||
"RerouteBeta": "Reroute Beta",
|
||||
"Scene": "Scène",
|
||||
"Server": "Serveur",
|
||||
"Server-Config": "Config-Serveur",
|
||||
"Settings": "Paramètres",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "Suivant",
|
||||
"selectUser": "Sélectionnez un utilisateur"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Adresse e-mail invalide",
|
||||
"maxLength": "Ne doit pas dépasser {length} caractères",
|
||||
"minLength": "Doit contenir au moins {length} caractères",
|
||||
"password": {
|
||||
"lowercase": "Doit contenir au moins une lettre minuscule",
|
||||
"match": "Les mots de passe doivent correspondre",
|
||||
"minLength": "Doit contenir entre 8 et 32 caractères",
|
||||
"number": "Doit contenir au moins un chiffre",
|
||||
"requirements": "Exigences du mot de passe",
|
||||
"special": "Doit contenir au moins un caractère spécial",
|
||||
"uppercase": "Doit contenir au moins une lettre majuscule"
|
||||
},
|
||||
"required": "Requis"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Commencer",
|
||||
"title": "Bienvenue sur ComfyUI"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "Droit"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Type de Caméra",
|
||||
"options": {
|
||||
"orthographic": "orthographique",
|
||||
"perspective": "perspective"
|
||||
},
|
||||
"tooltip": "Contrôle si la caméra est en perspective ou orthographique par défaut lorsqu'un nouveau widget 3D est créé. Ce défaut peut toujours être basculé individuellement pour chaque widget après sa création."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Afficher la Grille",
|
||||
"tooltip": "Basculer pour afficher la grille par défaut"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Afficher l'Aperçu",
|
||||
"tooltip": "Basculer pour afficher l'aperçu par défaut"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Langue"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "実行あたりのコスト",
|
||||
"title": "APIノード",
|
||||
"totalCost": "合計コスト"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "このワークフローにはAPIノードが含まれており、実行するためにはアカウントにサインインする必要があります。",
|
||||
"title": "APIノードを使用するためにはサインインが必要です"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "および",
|
||||
"confirmPasswordLabel": "パスワードの確認",
|
||||
"confirmPasswordPlaceholder": "もう一度同じパスワードを入力してください",
|
||||
"emailLabel": "メール",
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
"newUser": "新規ユーザーですか?",
|
||||
"orContinueWith": "または以下で続ける",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"privacyLink": "プライバシーポリシー",
|
||||
"signUp": "サインアップ",
|
||||
"success": "ログイン成功",
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
|
||||
"emailLabel": "メール",
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "新しいパスワードを入力してください",
|
||||
"signIn": "サインイン",
|
||||
"signUpButton": "サインアップ",
|
||||
"signUpWithGithub": "Githubでサインアップ",
|
||||
"signUpWithGoogle": "Googleでサインアップ",
|
||||
"title": "アカウントを作成する"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "クリップボードへのコピーに失敗しました",
|
||||
"errorNotSupported": "お使いのブラウザではクリップボードAPIがサポートされていません",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "インストール中",
|
||||
"interrupted": "中断されました",
|
||||
"keybinding": "キーバインディング",
|
||||
"learnMore": "詳細を学ぶ",
|
||||
"loadAllFolders": "すべてのフォルダーを読み込む",
|
||||
"loadWorkflow": "ワークフローを読み込む",
|
||||
"loading": "読み込み中",
|
||||
"login": "ログイン",
|
||||
"logs": "ログ",
|
||||
"migrate": "移行する",
|
||||
"missing": "不足している",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "トラブルシューティング"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "情報",
|
||||
"Appearance": "外観",
|
||||
"BrushAdjustment": "ブラシ調整",
|
||||
"Camera": "カメラ",
|
||||
"Canvas": "キャンバス",
|
||||
"ColorPalette": "カラーパレット",
|
||||
"Comfy": "Comfy",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "リンク",
|
||||
"LinkRelease": "リンク解除",
|
||||
"LiteGraph": "Lite Graph",
|
||||
"Load 3D": "3Dを読み込む",
|
||||
"Locale": "ロケール",
|
||||
"Mask Editor": "マスクエディタ",
|
||||
"Menu": "メニュー",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "キューボタン",
|
||||
"Reroute": "リルート",
|
||||
"RerouteBeta": "ルート変更ベータ",
|
||||
"Scene": "シーン",
|
||||
"Server": "サーバー",
|
||||
"Server-Config": "サーバー設定",
|
||||
"Settings": "設定",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "次へ",
|
||||
"selectUser": "ユーザーを選択"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
"minLength": "{length}文字以上でなければなりません",
|
||||
"password": {
|
||||
"lowercase": "少なくとも1つの小文字を含む必要があります",
|
||||
"match": "パスワードが一致する必要があります",
|
||||
"minLength": "8文字から32文字の間でなければなりません",
|
||||
"number": "少なくとも1つの数字を含む必要があります",
|
||||
"requirements": "パスワードの要件",
|
||||
"special": "少なくとも1つの特殊文字を含む必要があります",
|
||||
"uppercase": "少なくとも1つの大文字を含む必要があります"
|
||||
},
|
||||
"required": "必須"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "はじめる",
|
||||
"title": "ComfyUIへようこそ"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "ストレート"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "カメラタイプ",
|
||||
"options": {
|
||||
"orthographic": "オルソグラフィック",
|
||||
"perspective": "パースペクティブ"
|
||||
},
|
||||
"tooltip": "新しい3Dウィジェットが作成されたときに、デフォルトでカメラが透視投影か平行投影かを制御します。このデフォルトは、作成後に各ウィジェットごとに個別に切り替えることができます。"
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "グリッドを表示",
|
||||
"tooltip": "デフォルトでグリッドを表示するには切り替えます"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "プレビューを表示",
|
||||
"tooltip": "デフォルトでプレビューを表示するには切り替えます"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "言語"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "실행 당 비용",
|
||||
"title": "API 노드(들)",
|
||||
"totalCost": "총 비용"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "이 워크플로우에는 API 노드가 포함되어 있으며, 실행하려면 계정에 로그인해야 합니다.",
|
||||
"title": "API 노드 사용에 필요한 로그인"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "및",
|
||||
"confirmPasswordLabel": "비밀번호 확인",
|
||||
"confirmPasswordPlaceholder": "동일한 비밀번호를 다시 입력하세요",
|
||||
"emailLabel": "이메일",
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
"newUser": "처음이신가요?",
|
||||
"orContinueWith": "또는 다음으로 계속",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"privacyLink": "개인정보 보호정책",
|
||||
"signUp": "가입하기",
|
||||
"success": "로그인 성공",
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "이미 계정이 있으신가요?",
|
||||
"emailLabel": "이메일",
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "새 비밀번호를 입력하세요",
|
||||
"signIn": "로그인",
|
||||
"signUpButton": "가입하기",
|
||||
"signUpWithGithub": "Github로 가입하기",
|
||||
"signUpWithGoogle": "구글로 가입하기",
|
||||
"title": "계정 생성"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "클립보드에 복사하지 못했습니다",
|
||||
"errorNotSupported": "브라우저가 클립보드 API를 지원하지 않습니다.",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "설치 중",
|
||||
"interrupted": "중단됨",
|
||||
"keybinding": "키 바인딩",
|
||||
"learnMore": "더 알아보기",
|
||||
"loadAllFolders": "모든 폴더 로드",
|
||||
"loadWorkflow": "워크플로 로드",
|
||||
"loading": "로딩 중",
|
||||
"login": "로그인",
|
||||
"logs": "로그",
|
||||
"migrate": "이전(migrate)",
|
||||
"missing": "누락됨",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "문제 해결"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "정보",
|
||||
"Appearance": "모양",
|
||||
"BrushAdjustment": "브러시 조정",
|
||||
"Camera": "카메라",
|
||||
"Canvas": "캔버스",
|
||||
"ColorPalette": "색상 팔레트",
|
||||
"Comfy": "Comfy",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "링크",
|
||||
"LinkRelease": "링크 해제",
|
||||
"LiteGraph": "LiteGraph",
|
||||
"Load 3D": "3D 불러오기",
|
||||
"Locale": "언어 설정",
|
||||
"Mask Editor": "마스크 편집기",
|
||||
"Menu": "메뉴",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "실행 큐 버튼",
|
||||
"Reroute": "경유점",
|
||||
"RerouteBeta": "경유점 (베타)",
|
||||
"Scene": "장면",
|
||||
"Server": "서버",
|
||||
"Server-Config": "서버 구성",
|
||||
"Settings": "설정",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "다음",
|
||||
"selectUser": "사용자 선택"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
"minLength": "{length}자 이상이어야 합니다",
|
||||
"password": {
|
||||
"lowercase": "적어도 하나의 소문자를 포함해야 합니다",
|
||||
"match": "비밀번호가 일치해야 합니다",
|
||||
"minLength": "8자에서 32자 사이여야 합니다",
|
||||
"number": "적어도 하나의 숫자를 포함해야 합니다",
|
||||
"requirements": "비밀번호 요구사항",
|
||||
"special": "적어도 하나의 특수 문자를 포함해야 합니다",
|
||||
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
|
||||
},
|
||||
"required": "필수"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "시작하기",
|
||||
"title": "ComfyUI에 오신 것을 환영합니다"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "직선"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "카메라 유형",
|
||||
"options": {
|
||||
"orthographic": "직교법",
|
||||
"perspective": "원근법"
|
||||
},
|
||||
"tooltip": "새로운 3D 위젯이 생성될 때 카메라가 기본적으로 원근법 또는 직교법을 사용하는지를 제어합니다. 이 기본값은 생성 후 각 위젯별로 개별적으로 전환할 수 있습니다."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "그리드 표시",
|
||||
"tooltip": "기본적으로 그리드를 표시하도록 전환"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "미리보기 표시",
|
||||
"tooltip": "기본적으로 미리보기를 표시하도록 전환"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "언어"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "Стоимость за запуск",
|
||||
"title": "API Node(s)",
|
||||
"totalCost": "Общая стоимость"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "Этот рабочий процесс содержит API Nodes, которые требуют входа в вашу учетную запись для выполнения.",
|
||||
"title": "Требуется вход для использования API Nodes"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "и",
|
||||
"confirmPasswordLabel": "Подтвердите пароль",
|
||||
"confirmPasswordPlaceholder": "Введите тот же пароль еще раз",
|
||||
"emailLabel": "Электронная почта",
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
"newUser": "Вы здесь впервые?",
|
||||
"orContinueWith": "Или продолжить с",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"privacyLink": "Политикой конфиденциальности",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"success": "Вход выполнен успешно",
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"emailLabel": "Электронная почта",
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите новый пароль",
|
||||
"signIn": "Войти",
|
||||
"signUpButton": "Зарегистрироваться",
|
||||
"signUpWithGithub": "Зарегистрироваться через Github",
|
||||
"signUpWithGoogle": "Зарегистрироваться через Google",
|
||||
"title": "Создать аккаунт"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Не удалось скопировать в буфер обмена",
|
||||
"errorNotSupported": "API буфера обмена не поддерживается в вашем браузере",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "Установка",
|
||||
"interrupted": "Прервано",
|
||||
"keybinding": "Привязка клавиш",
|
||||
"learnMore": "Узнать больше",
|
||||
"loadAllFolders": "Загрузить все папки",
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
"loading": "Загрузка",
|
||||
"login": "Вход",
|
||||
"logs": "Логи",
|
||||
"migrate": "Мигрировать",
|
||||
"missing": "Отсутствует",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "Устранение неполадок"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "О программе",
|
||||
"Appearance": "Внешний вид",
|
||||
"BrushAdjustment": "Настройка кисти",
|
||||
"Camera": "Камера",
|
||||
"Canvas": "Холст",
|
||||
"ColorPalette": "Цветовая палитра",
|
||||
"Comfy": "Comfy",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "Ссылка",
|
||||
"LinkRelease": "Освобождение ссылки",
|
||||
"LiteGraph": "Lite Graph",
|
||||
"Load 3D": "Загрузить 3D",
|
||||
"Locale": "Локализация",
|
||||
"Mask Editor": "Редактор масок",
|
||||
"Menu": "Меню",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "Кнопка очереди",
|
||||
"Reroute": "Перенаправление",
|
||||
"RerouteBeta": "Бета-версия перенаправления",
|
||||
"Scene": "Сцена",
|
||||
"Server": "Сервер",
|
||||
"Server-Config": "Настройки сервера",
|
||||
"Settings": "Настройки",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "Далее",
|
||||
"selectUser": "Выберите пользователя"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
"minLength": "Должно быть не менее {length} символов",
|
||||
"password": {
|
||||
"lowercase": "Должен содержать хотя бы одну строчную букву",
|
||||
"match": "Пароли должны совпадать",
|
||||
"minLength": "Должно быть от 8 до 32 символов",
|
||||
"number": "Должен содержать хотя бы одну цифру",
|
||||
"requirements": "Требования к паролю",
|
||||
"special": "Должен содержать хотя бы один специальный символ",
|
||||
"uppercase": "Должен содержать хотя бы одну заглавную букву"
|
||||
},
|
||||
"required": "Обязательно"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Начать",
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "Прямой"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Тип камеры",
|
||||
"options": {
|
||||
"orthographic": "ортографическая",
|
||||
"perspective": "перспективная"
|
||||
},
|
||||
"tooltip": "Управляет тем, является ли камера перспективной или ортографической по умолчанию при создании нового 3D-виджета. Это значение по умолчанию все еще может быть переключено индивидуально для каждого виджета после его создания."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Показать сетку",
|
||||
"tooltip": "Переключиться, чтобы показывать сетку по умолчанию"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "Показать предварительный просмотр",
|
||||
"tooltip": "Переключиться, чтобы показывать предварительный просмотр по умолчанию"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "Язык"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
{
|
||||
"apiNodesCostBreakdown": {
|
||||
"costPerRun": "每次运行的成本",
|
||||
"title": "API节点",
|
||||
"totalCost": "总成本"
|
||||
},
|
||||
"apiNodesSignInDialog": {
|
||||
"message": "此工作流包含API节点,需要您登录账户才能运行。",
|
||||
"title": "使用API节点需要登录"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "和",
|
||||
"confirmPasswordLabel": "确认密码",
|
||||
"confirmPasswordPlaceholder": "再次输入相同的密码",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
"newUser": "新来的?",
|
||||
"orContinueWith": "或者继续使用",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"privacyLink": "隐私政策",
|
||||
"signUp": "注册",
|
||||
"success": "登录成功",
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "已经有账户了?",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入新密码",
|
||||
"signIn": "登录",
|
||||
"signUpButton": "注册",
|
||||
"signUpWithGithub": "使用Github注册",
|
||||
"signUpWithGoogle": "使用Google注册",
|
||||
"title": "创建一个账户"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "复制到剪贴板失败",
|
||||
"errorNotSupported": "您的浏览器不支持剪贴板API",
|
||||
@@ -172,9 +217,11 @@
|
||||
"installing": "正在安装",
|
||||
"interrupted": "已中断",
|
||||
"keybinding": "按键绑定",
|
||||
"learnMore": "了解更多",
|
||||
"loadAllFolders": "加载所有文件夹",
|
||||
"loadWorkflow": "加载工作流",
|
||||
"loading": "加载中",
|
||||
"login": "登录",
|
||||
"logs": "日志",
|
||||
"migrate": "迁移",
|
||||
"missing": "缺失",
|
||||
@@ -812,9 +859,11 @@
|
||||
"troubleshoot": "故障排除"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
"BrushAdjustment": "画笔调整",
|
||||
"Camera": "相机",
|
||||
"Canvas": "画布",
|
||||
"ColorPalette": "色彩主题",
|
||||
"Comfy": "Comfy",
|
||||
@@ -831,6 +880,7 @@
|
||||
"Link": "连线",
|
||||
"LinkRelease": "释放链接",
|
||||
"LiteGraph": "画面",
|
||||
"Load 3D": "加载3D",
|
||||
"Locale": "区域设置",
|
||||
"Mask Editor": "遮罩编辑器",
|
||||
"Menu": "菜单",
|
||||
@@ -845,6 +895,7 @@
|
||||
"QueueButton": "执行按钮",
|
||||
"Reroute": "重新路由",
|
||||
"RerouteBeta": "转接点 Beta",
|
||||
"Scene": "场景",
|
||||
"Server": "服务器",
|
||||
"Server-Config": "服务器配置",
|
||||
"Settings": "设置",
|
||||
@@ -1028,6 +1079,21 @@
|
||||
"next": "下一步",
|
||||
"selectUser": "选择用户"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
"minLength": "必须至少有{length}个字符",
|
||||
"password": {
|
||||
"lowercase": "必须包含至少一个小写字母",
|
||||
"match": "密码必须匹配",
|
||||
"minLength": "必须在8到32个字符之间",
|
||||
"number": "必须包含至少一个数字",
|
||||
"requirements": "密码要求",
|
||||
"special": "必须包含至少一个特殊字符",
|
||||
"uppercase": "必须包含至少一个大写字母"
|
||||
},
|
||||
"required": "必填"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "开始使用",
|
||||
"title": "欢迎使用 ComfyUI"
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"Straight": "直角线"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "摄像机类型",
|
||||
"options": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透视"
|
||||
},
|
||||
"tooltip": "控制创建新的3D小部件时,默认的相机是透视还是正交。这个默认设置仍然可以在创建后为每个小部件单独切换。"
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "显示网格",
|
||||
"tooltip": "默认显示网格开关"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "显示预览",
|
||||
"tooltip": "默认显示预览开关"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "语言"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import '@comfyorg/litegraph/style.css'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,8 +10,10 @@ 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 '@/assets/css/style.css'
|
||||
import { FIREBASE_CONFIG } from '@/config/firebase'
|
||||
import router from '@/router'
|
||||
|
||||
import App from './App.vue'
|
||||
@@ -23,6 +26,8 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
}
|
||||
})
|
||||
|
||||
const firebaseApp = initializeApp(FIREBASE_CONFIG)
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
Sentry.init({
|
||||
@@ -58,4 +63,8 @@ app
|
||||
.use(ToastService)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(VueFire, {
|
||||
firebaseApp,
|
||||
modules: [VueFireAuth()]
|
||||
})
|
||||
.mount('#vue-app')
|
||||
|
||||
@@ -440,6 +440,9 @@ const zSettings = z.object({
|
||||
'Comfy.MaskEditor.UseNewEditor': z.boolean(),
|
||||
'Comfy.MaskEditor.BrushAdjustmentSpeed': z.number(),
|
||||
'Comfy.MaskEditor.UseDominantAxis': z.boolean(),
|
||||
'Comfy.Load3D.ShowGrid': z.boolean(),
|
||||
'Comfy.Load3D.ShowPreview': z.boolean(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.boolean(),
|
||||
|
||||
36
src/schemas/signInSchema.ts
Normal file
36
src/schemas/signInSchema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export const signInSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email(t('validation.invalidEmail'))
|
||||
.min(1, t('validation.required')),
|
||||
password: z.string().min(1, t('validation.required'))
|
||||
})
|
||||
|
||||
export type SignInData = z.infer<typeof signInSchema>
|
||||
|
||||
export const signUpSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email(t('validation.invalidEmail'))
|
||||
.min(1, t('validation.required')),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, t('validation.minLength', { length: 8 }))
|
||||
.max(32, t('validation.maxLength', { length: 32 }))
|
||||
.regex(/[A-Z]/, t('validation.password.uppercase'))
|
||||
.regex(/[a-z]/, t('validation.password.lowercase'))
|
||||
.regex(/\d/, t('validation.password.number'))
|
||||
.regex(/[^A-Za-z0-9]/, t('validation.password.special')),
|
||||
confirmPassword: z.string().min(1, t('validation.required'))
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: t('validation.password.match'),
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
|
||||
export type SignUpData = z.infer<typeof signUpSchema>
|
||||
@@ -1,3 +1,4 @@
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
@@ -6,9 +7,11 @@ import ManagerProgressDialogContent from '@/components/dialog/content/ManagerPro
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
|
||||
import ManagerProgressHeader from '@/components/dialog/header/ManagerProgressHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
@@ -16,6 +19,7 @@ import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkfl
|
||||
import { t } from '@/i18n'
|
||||
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
|
||||
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
import { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
@@ -216,6 +220,54 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog requiring sign in for API nodes
|
||||
* @returns Promise that resolves to true if user clicks login, false if cancelled
|
||||
*/
|
||||
async function showApiNodesSignInDialog(
|
||||
apiNodes: ApiNodeCost[]
|
||||
): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'api-nodes-signin',
|
||||
component: ApiNodesSignInContent,
|
||||
props: {
|
||||
apiNodes,
|
||||
onLogin: () => showSignInDialog().then((result) => resolve(result)),
|
||||
onCancel: () => resolve(false)
|
||||
},
|
||||
headerComponent: ComfyOrgHeader,
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
onClose: () => resolve(false)
|
||||
}
|
||||
})
|
||||
}).then((result) => {
|
||||
dialogStore.closeDialog({ key: 'api-nodes-signin' })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function showSignInDialog(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-signin',
|
||||
component: SignInContent,
|
||||
headerComponent: ComfyOrgHeader,
|
||||
props: {
|
||||
onSuccess: () => resolve(true)
|
||||
},
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
onClose: () => resolve(false)
|
||||
}
|
||||
})
|
||||
}).then((result) => {
|
||||
dialogStore.closeDialog({ key: 'global-signin' })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function prompt({
|
||||
title,
|
||||
message,
|
||||
@@ -300,6 +352,8 @@ export const useDialogService = () => {
|
||||
showManagerDialog,
|
||||
showManagerProgressDialog,
|
||||
showErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
118
src/stores/firebaseAuthStore.ts
Normal file
118
src/stores/firebaseAuthStore.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
type Auth,
|
||||
GithubAuthProvider,
|
||||
GoogleAuthProvider,
|
||||
type User,
|
||||
type UserCredential,
|
||||
createUserWithEmailAndPassword,
|
||||
onAuthStateChanged,
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
signOut
|
||||
} from 'firebase/auth'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Providers
|
||||
const googleProvider = new GoogleAuthProvider()
|
||||
const githubProvider = new GithubAuthProvider()
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!currentUser.value)
|
||||
const userEmail = computed(() => currentUser.value?.email)
|
||||
const userId = computed(() => currentUser.value?.uid)
|
||||
|
||||
// Get auth from VueFire and listen for auth state changes
|
||||
const auth = useFirebaseAuth()
|
||||
if (auth) {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
})
|
||||
} else {
|
||||
error.value = 'Firebase Auth not available from VueFire'
|
||||
}
|
||||
|
||||
const executeAuthAction = async <T>(
|
||||
action: (auth: Auth) => Promise<T>
|
||||
): Promise<T> => {
|
||||
if (!auth) throw new Error('Firebase Auth not initialized')
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
return await action(auth)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const login = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithEmailAndPassword(authInstance, email, password)
|
||||
)
|
||||
|
||||
const register = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
createUserWithEmailAndPassword(authInstance, email, password)
|
||||
)
|
||||
|
||||
const loginWithGoogle = async (): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithPopup(authInstance, googleProvider)
|
||||
)
|
||||
|
||||
const loginWithGithub = async (): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithPopup(authInstance, githubProvider)
|
||||
)
|
||||
|
||||
const logout = async (): Promise<void> =>
|
||||
executeAuthAction((authInstance) => signOut(authInstance))
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
return currentUser.value.getIdToken()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
currentUser,
|
||||
isInitialized,
|
||||
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
userEmail,
|
||||
userId,
|
||||
|
||||
// Actions
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getIdToken,
|
||||
loginWithGoogle,
|
||||
loginWithGithub
|
||||
}
|
||||
})
|
||||
4
src/types/apiNodeTypes.ts
Normal file
4
src/types/apiNodeTypes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ApiNodeCost {
|
||||
name: string
|
||||
cost: number
|
||||
}
|
||||
@@ -21,6 +21,43 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/customer': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Create a new customer
|
||||
* @description Creates a new customer using the provided token. No request body is needed as user information is extracted from the token.
|
||||
*/
|
||||
post: operations['createCustomer']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/customer/credit': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Initiates a Credit Purchase. */
|
||||
post: operations['InitiateCreditPurchase']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/upload-artifact': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -900,6 +937,98 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/ideogram/generate': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Proxy request to Ideogram for image generation
|
||||
* @description Forwards image generation requests to Ideogram's API and returns the results.
|
||||
*/
|
||||
post: operations['ideogramGenerate']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/webhook/metronome/zero-balance': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** receive alert on remaining balance is 0 */
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
properties?: {
|
||||
/** @description the metronome customer id */
|
||||
customer_id?: string
|
||||
/** @description the customer remaining balance */
|
||||
remaining_balance?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Webhook processed succesfully */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal Server Error (proxy or upstream issue) */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
}
|
||||
export type webhooks = Record<string, never>
|
||||
export interface components {
|
||||
@@ -1225,6 +1354,81 @@ export interface components {
|
||||
/** @description The pip freeze output */
|
||||
pip_freeze?: string
|
||||
}
|
||||
Customer: {
|
||||
/** @description The firebase UID of the user */
|
||||
id: string
|
||||
/** @description The email address for this user */
|
||||
email?: string
|
||||
/** @description The name for this user */
|
||||
name?: string
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description The date and time the user was created
|
||||
*/
|
||||
createdAt?: string
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description The date and time the user was last updated
|
||||
*/
|
||||
updatedAt?: string
|
||||
}
|
||||
/** @description Parameters for the Ideogram generation proxy request. Based on Ideogram's API. */
|
||||
IdeogramGenerateRequest: {
|
||||
/** @description The image generation request parameters. */
|
||||
image_request: {
|
||||
/** @description Required. The prompt to use to generate the image. */
|
||||
prompt: string
|
||||
/** @description Optional. The aspect ratio (e.g., 'ASPECT_16_9', 'ASPECT_1_1'). Cannot be used with resolution. Defaults to 'ASPECT_1_1' if unspecified. */
|
||||
aspect_ratio?: string
|
||||
/** @description Optional. The model used (e.g., 'V_2', 'V_2A_TURBO'). Defaults to 'V_2' if unspecified. */
|
||||
model?: string
|
||||
/** @description Optional. MagicPrompt usage ('AUTO', 'ON', 'OFF'). */
|
||||
magic_prompt_option?: string
|
||||
/**
|
||||
* Format: int64
|
||||
* @description Optional. A number between 0 and 2147483647.
|
||||
*/
|
||||
seed?: number
|
||||
/** @description Optional. Style type ('AUTO', 'GENERAL', 'REALISTIC', 'DESIGN', 'RENDER_3D', 'ANIME'). Only for models V_2 and above. */
|
||||
style_type?: string
|
||||
/** @description Optional. Description of what to exclude. Only for V_1, V_1_TURBO, V_2, V_2_TURBO. */
|
||||
negative_prompt?: string
|
||||
/**
|
||||
* @description Optional. Number of images to generate (1-8). Defaults to 1.
|
||||
* @default 1
|
||||
*/
|
||||
num_images: number
|
||||
/** @description Optional. Resolution (e.g., 'RESOLUTION_1024_1024'). Only for model V_2. Cannot be used with aspect_ratio. */
|
||||
resolution?: string
|
||||
/** @description Optional. Color palette object. Only for V_2, V_2_TURBO. */
|
||||
color_palette?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @description Response from the Ideogram image generation API. */
|
||||
IdeogramGenerateResponse: {
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Timestamp when the generation was created.
|
||||
*/
|
||||
created?: string
|
||||
/** @description Array of generated image information. */
|
||||
data?: {
|
||||
/** @description The prompt used to generate this image. */
|
||||
prompt?: string
|
||||
/** @description The resolution of the generated image (e.g., '1024x1024'). */
|
||||
resolution?: string
|
||||
/** @description Indicates whether the image is considered safe. */
|
||||
is_image_safe?: boolean
|
||||
/** @description The seed value used for this generation. */
|
||||
seed?: number
|
||||
/** @description URL to the generated image. */
|
||||
url?: string
|
||||
/** @description The style type used for generation (e.g., 'REALISTIC', 'ANIME'). */
|
||||
style_type?: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
responses: never
|
||||
parameters: never
|
||||
@@ -1268,6 +1472,111 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
createCustomer: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
/** @description Customer created successfully */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['Customer']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid token or user already exists */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized or invalid token */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InitiateCreditPurchase: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/**
|
||||
* Format: int64
|
||||
* @description the amount of the checkout transaction in micro value
|
||||
*/
|
||||
amount_micros?: number
|
||||
/** @description the currency used in the checkout transaction */
|
||||
currency?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Customer Checkout created successfully */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @description the url to redirect the customer */
|
||||
checkout_url?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid token or user already exists */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized or invalid token */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getWorkflowResult: {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -3417,4 +3726,80 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
ideogramGenerate: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Successful response from Ideogram proxy */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Request (invalid input to proxy) */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Rate limit exceeded (either from proxy or Ideogram) */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal Server Error (proxy or upstream issue) */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Gateway (error communicating with Ideogram) */
|
||||
502: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Gateway Timeout (Ideogram took too long to respond) */
|
||||
504: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
364
tests-ui/tests/store/firebaseAuthStore.test.ts
Normal file
364
tests-ui/tests/store/firebaseAuthStore.test.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import * as firebaseAuth from 'firebase/auth'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vuefire from 'vuefire'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
vi.mock('vuefire', () => ({
|
||||
useFirebaseAuth: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
signInWithEmailAndPassword: vi.fn(),
|
||||
createUserWithEmailAndPassword: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
onAuthStateChanged: vi.fn(),
|
||||
signInWithPopup: vi.fn(),
|
||||
GoogleAuthProvider: vi.fn(),
|
||||
GithubAuthProvider: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useFirebaseAuthStore', () => {
|
||||
let store: ReturnType<typeof useFirebaseAuthStore>
|
||||
let authStateCallback: (user: any) => void
|
||||
|
||||
const mockAuth = {
|
||||
/* mock Auth object */
|
||||
}
|
||||
|
||||
const mockUser = {
|
||||
uid: 'test-user-id',
|
||||
email: 'test@example.com',
|
||||
getIdToken: vi.fn().mockResolvedValue('mock-id-token')
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
|
||||
// Mock useFirebaseAuth to return our mock auth object
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
|
||||
|
||||
// Mock onAuthStateChanged to capture the callback and simulate initial auth state
|
||||
vi.mocked(firebaseAuth.onAuthStateChanged).mockImplementation(
|
||||
(_, callback) => {
|
||||
authStateCallback = callback as (user: any) => void
|
||||
// Call the callback with our mock user
|
||||
;(callback as (user: any) => void)(mockUser)
|
||||
// Return an unsubscribe function
|
||||
return vi.fn()
|
||||
}
|
||||
)
|
||||
|
||||
// Initialize Pinia
|
||||
setActivePinia(createPinia())
|
||||
store = useFirebaseAuthStore()
|
||||
})
|
||||
|
||||
it('should initialize with the current user', () => {
|
||||
expect(store.currentUser).toEqual(mockUser)
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
expect(store.userEmail).toBe('test@example.com')
|
||||
expect(store.userId).toBe('test-user-id')
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should properly clean up error state between operations', async () => {
|
||||
// First, cause an error
|
||||
const mockError = new Error('Invalid password')
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockRejectedValueOnce(
|
||||
mockError
|
||||
)
|
||||
|
||||
try {
|
||||
await store.login('test@example.com', 'wrong-password')
|
||||
} catch (e) {
|
||||
// Error expected
|
||||
}
|
||||
|
||||
expect(store.error).toBe('Invalid password')
|
||||
|
||||
// Now, succeed on next attempt
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({
|
||||
user: mockUser
|
||||
} as any)
|
||||
|
||||
await store.login('test@example.com', 'correct-password')
|
||||
|
||||
// Error should be cleared
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle auth initialization failure', async () => {
|
||||
// Mock auth as null to simulate initialization failure
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(null)
|
||||
|
||||
// Create a new store instance
|
||||
setActivePinia(createPinia())
|
||||
const uninitializedStore = useFirebaseAuthStore()
|
||||
|
||||
// Check that isInitialized is false
|
||||
expect(uninitializedStore.isInitialized).toBe(false)
|
||||
|
||||
// Verify store actions throw appropriate errors
|
||||
await expect(
|
||||
uninitializedStore.login('test@example.com', 'password')
|
||||
).rejects.toThrow('Firebase Auth not initialized')
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('should login with valid credentials', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.login('test@example.com', 'password')
|
||||
|
||||
expect(firebaseAuth.signInWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'test@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle login errors', async () => {
|
||||
const mockError = new Error('Invalid password')
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockRejectedValue(
|
||||
mockError
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.login('test@example.com', 'wrong-password')
|
||||
).rejects.toThrow('Invalid password')
|
||||
|
||||
expect(firebaseAuth.signInWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'test@example.com',
|
||||
'wrong-password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Invalid password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.createUserWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.register('new@example.com', 'password')
|
||||
|
||||
expect(firebaseAuth.createUserWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'new@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle registration errors', async () => {
|
||||
const mockError = new Error('Email already in use')
|
||||
vi.mocked(firebaseAuth.createUserWithEmailAndPassword).mockRejectedValue(
|
||||
mockError
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.register('existing@example.com', 'password')
|
||||
).rejects.toThrow('Email already in use')
|
||||
|
||||
expect(firebaseAuth.createUserWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'existing@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Email already in use')
|
||||
})
|
||||
|
||||
it('should handle concurrent login attempts correctly', async () => {
|
||||
// Set up multiple login promises
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const loginPromise1 = store.login('user1@example.com', 'password1')
|
||||
const loginPromise2 = store.login('user2@example.com', 'password2')
|
||||
|
||||
// Resolve both promises
|
||||
await Promise.all([loginPromise1, loginPromise2])
|
||||
|
||||
// Verify the loading state is reset
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout', () => {
|
||||
it('should sign out the user', async () => {
|
||||
vi.mocked(firebaseAuth.signOut).mockResolvedValue(undefined)
|
||||
|
||||
await store.logout()
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
})
|
||||
|
||||
it('should handle logout errors', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
vi.mocked(firebaseAuth.signOut).mockRejectedValue(mockError)
|
||||
|
||||
await expect(store.logout()).rejects.toThrow('Network error')
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
expect(store.error).toBe('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getIdToken', () => {
|
||||
it('should return the user ID token', async () => {
|
||||
// FIX 2: Reset the mock and set a specific return value
|
||||
mockUser.getIdToken.mockReset()
|
||||
mockUser.getIdToken.mockResolvedValue('mock-id-token')
|
||||
|
||||
const token = await store.getIdToken()
|
||||
|
||||
expect(mockUser.getIdToken).toHaveBeenCalled()
|
||||
expect(token).toBe('mock-id-token')
|
||||
})
|
||||
|
||||
it('should return null when no user is logged in', async () => {
|
||||
// Simulate logged out state
|
||||
authStateCallback(null)
|
||||
|
||||
const token = await store.getIdToken()
|
||||
|
||||
expect(token).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for token after login and logout sequence', async () => {
|
||||
// Setup mock for login
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
// Login
|
||||
await store.login('test@example.com', 'password')
|
||||
|
||||
// Simulate successful auth state update after login
|
||||
authStateCallback(mockUser)
|
||||
|
||||
// Verify we're logged in and can get a token
|
||||
mockUser.getIdToken.mockReset()
|
||||
mockUser.getIdToken.mockResolvedValue('mock-id-token')
|
||||
expect(await store.getIdToken()).toBe('mock-id-token')
|
||||
|
||||
// Setup mock for logout
|
||||
vi.mocked(firebaseAuth.signOut).mockResolvedValue(undefined)
|
||||
|
||||
// Logout
|
||||
await store.logout()
|
||||
|
||||
// Simulate successful auth state update after logout
|
||||
authStateCallback(null)
|
||||
|
||||
// Verify token is null after logout
|
||||
const tokenAfterLogout = await store.getIdToken()
|
||||
expect(tokenAfterLogout).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('social authentication', () => {
|
||||
describe('loginWithGoogle', () => {
|
||||
it('should sign in with Google', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.loginWithGoogle()
|
||||
|
||||
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
expect.any(firebaseAuth.GoogleAuthProvider)
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Google sign in errors', async () => {
|
||||
const mockError = new Error('Google authentication failed')
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError)
|
||||
|
||||
await expect(store.loginWithGoogle()).rejects.toThrow(
|
||||
'Google authentication failed'
|
||||
)
|
||||
|
||||
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
expect.any(firebaseAuth.GoogleAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Google authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('loginWithGithub', () => {
|
||||
it('should sign in with Github', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.loginWithGithub()
|
||||
|
||||
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
expect.any(firebaseAuth.GithubAuthProvider)
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Github sign in errors', async () => {
|
||||
const mockError = new Error('Github authentication failed')
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError)
|
||||
|
||||
await expect(store.loginWithGithub()).rejects.toThrow(
|
||||
'Github authentication failed'
|
||||
)
|
||||
|
||||
expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
expect.any(firebaseAuth.GithubAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Github authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle concurrent social login attempts correctly', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const googleLoginPromise = store.loginWithGoogle()
|
||||
const githubLoginPromise = store.loginWithGithub()
|
||||
|
||||
await Promise.all([googleLoginPromise, githubLoginPromise])
|
||||
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@
|
||||
"incremental": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user