mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 18:07:35 +00:00
Compare commits
1 Commits
remove-cac
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda3929fed |
@@ -55,7 +55,10 @@ const config: KnipConfig = {
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// Loaded via @plugin directive in CSS, not detected by knip
|
||||
'packages/design-system/src/css/lucideStrokePlugin.js'
|
||||
'packages/design-system/src/css/lucideStrokePlugin.js',
|
||||
// Pending integration in stacked PR (feat/storybook-color-picker)
|
||||
'src/components/ui/color-picker/ColorPickerSaturationValue.vue',
|
||||
'src/components/ui/color-picker/ColorPickerSlider.vue'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { hue } = defineProps<{
|
||||
hue: number
|
||||
}>()
|
||||
|
||||
const saturation = defineModel<number>('saturation', { required: true })
|
||||
const value = defineModel<number>('value', { required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const hueBackground = computed(() => `hsl(${hue}, 100%, 50%)`)
|
||||
|
||||
const handleStyle = computed(() => ({
|
||||
left: `${saturation.value}%`,
|
||||
top: `${100 - value.value}%`
|
||||
}))
|
||||
|
||||
function clamp(v: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, v))
|
||||
}
|
||||
|
||||
function updateFromPointer(e: PointerEvent) {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
|
||||
saturation.value = Math.round(x * 100)
|
||||
value.value = Math.round((1 - y) * 100)
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const step = e.shiftKey ? 10 : 1
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
saturation.value = clamp(saturation.value - step, 0, 100)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
saturation.value = clamp(saturation.value + step, 0, 100)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
value.value = clamp(value.value + step, 0, 100)
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
value.value = clamp(value.value - step, 0, 100)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
:aria-label="t('colorPicker.saturationValue')"
|
||||
:aria-valuetext="
|
||||
t('colorPicker.saturationBrightnessValue', {
|
||||
saturation: saturation,
|
||||
value: value
|
||||
})
|
||||
"
|
||||
class="relative aspect-square w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||
:style="{ backgroundColor: hueBackground, touchAction: 'none' }"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 rounded-sm bg-linear-to-r from-white to-transparent"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 rounded-sm bg-linear-to-b from-transparent to-black"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute size-3.5 -translate-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||
:style="handleStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
115
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
115
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { hsbToRgb, rgbToHex } from '@/utils/colorUtil'
|
||||
|
||||
const {
|
||||
type,
|
||||
hue = 0,
|
||||
saturation = 100,
|
||||
brightness = 100
|
||||
} = defineProps<{
|
||||
type: 'hue' | 'alpha'
|
||||
hue?: number
|
||||
saturation?: number
|
||||
brightness?: number
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const max = computed(() => (type === 'hue' ? 360 : 100))
|
||||
|
||||
const fraction = computed(() => modelValue.value / max.value)
|
||||
|
||||
const trackBackground = computed(() => {
|
||||
if (type === 'hue') {
|
||||
return 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)'
|
||||
}
|
||||
const rgb = hsbToRgb({ h: hue, s: saturation, b: brightness })
|
||||
const hex = rgbToHex(rgb)
|
||||
return `linear-gradient(to right, transparent, ${hex})`
|
||||
})
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (type === 'alpha') {
|
||||
return {
|
||||
backgroundImage:
|
||||
'repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%)',
|
||||
backgroundSize: '8px 8px',
|
||||
touchAction: 'none'
|
||||
}
|
||||
}
|
||||
return {
|
||||
background: trackBackground.value,
|
||||
touchAction: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
const ariaLabel = computed(() =>
|
||||
type === 'hue' ? t('colorPicker.hue') : t('colorPicker.alpha')
|
||||
)
|
||||
|
||||
function clamp(v: number, min: number, maxVal: number) {
|
||||
return Math.max(min, Math.min(maxVal, v))
|
||||
}
|
||||
|
||||
function updateFromPointer(e: PointerEvent) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
modelValue.value = Math.round(x * max.value)
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const step = e.shiftKey ? 10 : 1
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
modelValue.value = clamp(modelValue.value - step, 0, max.value)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
modelValue.value = clamp(modelValue.value + step, 0, max.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
:aria-label="ariaLabel"
|
||||
:aria-valuenow="modelValue"
|
||||
:aria-valuemin="0"
|
||||
:aria-valuemax="max"
|
||||
class="relative flex h-4 cursor-pointer items-center rounded-full p-px outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||
:style="containerStyle"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div
|
||||
v-if="type === 'alpha'"
|
||||
class="absolute inset-0 rounded-full"
|
||||
:style="{ background: trackBackground }"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute aspect-square h-full -translate-x-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||
:style="{ left: `${fraction * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,7 +59,10 @@ function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
|
||||
}
|
||||
|
||||
const pathPlusQueryParams = api.apiURL(
|
||||
'/view?' + params.toString() + app.getPreviewFormatParam()
|
||||
'/view?' +
|
||||
params.toString() +
|
||||
app.getPreviewFormatParam() +
|
||||
app.getRandParam()
|
||||
)
|
||||
const imageElement = new Image()
|
||||
imageElement.crossOrigin = 'anonymous'
|
||||
|
||||
@@ -17,7 +17,7 @@ type MockTask = {
|
||||
executionEndTimestamp?: number
|
||||
previewOutput?: {
|
||||
isImage: boolean
|
||||
url: string
|
||||
urlWithTimestamp: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ describe(useQueueNotificationBanners, () => {
|
||||
if (previewUrl) {
|
||||
task.previewOutput = {
|
||||
isImage,
|
||||
url: previewUrl
|
||||
urlWithTimestamp: previewUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ export const useQueueNotificationBanners = () => {
|
||||
completedCount++
|
||||
const preview = task.previewOutput
|
||||
if (preview?.isImage) {
|
||||
imagePreviews.push(preview.url)
|
||||
imagePreviews.push(preview.urlWithTimestamp)
|
||||
}
|
||||
} else if (state === 'failed') {
|
||||
failedCount++
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeOutputWith } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
type ImageCompareOutput = NodeOutputWith<{
|
||||
@@ -23,10 +24,11 @@ useExtensionService().registerExtension({
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const { a_images: aImages, b_images: bImages } = output
|
||||
const rand = app.getRandParam()
|
||||
|
||||
const toUrl = (record: Record<string, string>) => {
|
||||
const params = new URLSearchParams(record)
|
||||
return api.apiURL(`/view?${params}`)
|
||||
return api.apiURL(`/view?${params}${rand}`)
|
||||
}
|
||||
|
||||
const beforeImages =
|
||||
|
||||
@@ -2,6 +2,7 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
class Load3dUtils {
|
||||
static async generateThumbnailIfNeeded(
|
||||
@@ -132,7 +133,8 @@ class Load3dUtils {
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
|
||||
@@ -498,6 +498,14 @@
|
||||
"black": "Black",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"colorPicker": {
|
||||
"saturationValue": "Saturation and brightness",
|
||||
"saturationBrightnessValue": "Saturation {saturation}%, brightness {value}%",
|
||||
"saturation": "Saturation",
|
||||
"brightness": "Brightness",
|
||||
"hue": "Hue",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"contextMenu": {
|
||||
"Inputs": "Inputs",
|
||||
"Outputs": "Outputs",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<WidgetLayoutField v-slot="{ borderStyle }" :widget :no-border="!hasLabels">
|
||||
<WidgetLayoutField :widget>
|
||||
<!-- Use ToggleGroup when explicit labels are provided -->
|
||||
<ToggleGroup
|
||||
v-if="hasLabels"
|
||||
@@ -25,13 +25,7 @@
|
||||
<!-- Use ToggleSwitch for implicit boolean states -->
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
'-m-1 flex w-fit items-center gap-2 rounded-full p-1',
|
||||
hideLayoutField || 'ml-auto',
|
||||
borderStyle
|
||||
)
|
||||
"
|
||||
:class="cn('flex w-fit items-center gap-2', hideLayoutField || 'ml-auto')"
|
||||
>
|
||||
<ToggleSwitch
|
||||
v-model="modelValue"
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { useHideLayoutField } from '@/types/widgetTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { widget, rootClass } = defineProps<{
|
||||
const { rootClass } = defineProps<{
|
||||
widget: Pick<
|
||||
SimplifiedWidget<string | number | undefined>,
|
||||
'name' | 'label' | 'borderStyle'
|
||||
>
|
||||
rootClass?: string
|
||||
noBorder?: boolean
|
||||
}>()
|
||||
|
||||
const hideLayoutField = useHideLayoutField()
|
||||
const borderStyle = computed(() =>
|
||||
cn(
|
||||
'focus-within:ring focus-within:ring-component-node-widget-background-highlighted',
|
||||
widget.borderStyle
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,15 +33,15 @@ const borderStyle = computed(() =>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'min-w-0 cursor-default rounded-lg transition-all',
|
||||
!noBorder && borderStyle
|
||||
'min-w-0 cursor-default rounded-lg transition-all has-focus-visible:ring has-focus-visible:ring-component-node-widget-background-highlighted',
|
||||
widget.borderStyle
|
||||
)
|
||||
"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
>
|
||||
<slot :border-style />
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
/**
|
||||
* Format time in MM:SS format
|
||||
@@ -19,7 +20,8 @@ export function getResourceURL(
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
|
||||
@@ -382,6 +382,11 @@ export class ComfyApp {
|
||||
else return ''
|
||||
}
|
||||
|
||||
getRandParam() {
|
||||
if (isCloud) return ''
|
||||
return '&rand=' + Math.random()
|
||||
}
|
||||
|
||||
static onClipspaceEditorSave() {
|
||||
if (ComfyApp.clipspace_return_node) {
|
||||
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node)
|
||||
|
||||
@@ -117,11 +117,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const outputs = getNodeOutputs(node)
|
||||
if (!outputs?.images?.length) return
|
||||
|
||||
const rand = app.getRandParam()
|
||||
const previewParam = getPreviewParam(node, outputs)
|
||||
|
||||
return outputs.images.map((image) => {
|
||||
const params = new URLSearchParams(image)
|
||||
return api.apiURL(`/view?${params}${previewParam}`)
|
||||
return api.apiURL(`/view?${params}${previewParam}${rand}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@ export class ResultItemImpl {
|
||||
return api.apiURL('/view?' + params)
|
||||
}
|
||||
|
||||
get urlWithTimestamp(): string {
|
||||
return `${this.url}&t=${+new Date()}`
|
||||
}
|
||||
|
||||
get isVhsFormat(): boolean {
|
||||
return !!this.format && !!this.frame_rate
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user