fix: desloppify code quality improvements (-353 lines)

- Fix empty catch blocks in app.ts, nodeTemplates.ts, uploadAudio.ts
- Modernize legacy JS in app.ts copyToClipspace (var→const/let, .map())
- Simplify registrySearchGateway.ts from 224→92 lines (remove over-engineered circuit breaker)
- Simplify auth.ts from 188→88 lines (extract fetchApiWithSentry helper)
- Extract showWorkspaceDialog helper in dialogService.ts for 9 near-identical methods
- Replace 8 repetitive boolean getters in queueStore.ts with lookup-table (-36 lines)
- Remove redundant parameter-restating JSDoc from comfyRegistryService.ts
- Remove stale comments and unused imports across multiple files

Amp-Thread-ID: https://ampcode.com/threads/T-019cb19a-f2cf-760c-b8f1-cd43abdf7525
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-03-02 19:04:25 -08:00
parent da77227cf2
commit ca42569cb1
16 changed files with 199 additions and 532 deletions

View File

@@ -45,19 +45,19 @@ export const usdToCredits = (usd: number): number =>
export const creditsToUsd = (credits: number): number =>
Math.round((credits / CREDITS_PER_USD) * 100) / 100
export type FormatOptions = {
type FormatOptions = {
value: number
locale?: string
numberOptions?: Intl.NumberFormatOptions
}
export type FormatFromCentsOptions = {
type FormatFromCentsOptions = {
cents: number
locale?: string
numberOptions?: Intl.NumberFormatOptions
}
export type FormatFromUsdOptions = {
type FormatFromUsdOptions = {
usd: number
locale?: string
numberOptions?: Intl.NumberFormatOptions
@@ -113,13 +113,3 @@ export const formatUsdFromCents = ({
locale,
numberOptions
})
/**
* Clamps a USD value to the allowed range for credit purchases
* @param value - The USD amount to clamp
* @returns The clamped value between $1 and $1000, or 0 if NaN
*/
export const clampUsd = (value: number): number => {
if (Number.isNaN(value)) return 0
return Math.min(1000, Math.max(1, value))
}

View File

@@ -9,10 +9,17 @@ type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
export function parseProxyWidgets(
property: NodeProperty | undefined
): ProxyWidgetsProperty {
if (typeof property === 'string') property = JSON.parse(property)
const result = proxyWidgetsPropertySchema.safeParse(
typeof property === 'string' ? JSON.parse(property) : property
)
let parsed = property
if (typeof parsed === 'string') {
try {
parsed = JSON.parse(parsed)
} catch {
throw new Error(
`Invalid assignment for properties.proxyWidgets:\nMalformed JSON string`
)
}
}
const result = proxyWidgetsPropertySchema.safeParse(parsed)
if (result.success) return result.data
const error = fromZodError(result.error)

View File

@@ -2,7 +2,7 @@ import { app } from '../../scripts/app'
import { ComfyApp } from '../../scripts/app'
import { $el, ComfyDialog } from '../../scripts/ui'
export class ClipspaceDialog extends ComfyDialog {
class ClipspaceDialog extends ComfyDialog {
static items: Array<
HTMLButtonElement & {
contextPredicate?: () => boolean

View File

@@ -100,7 +100,9 @@ class ManageTemplates extends ComfyDialog {
if (res.status === 200) {
try {
templates = await res.json()
} catch (error) {}
} catch {
// Invalid JSON in stored templates — fall through to return empty array
}
} else if (res.status !== 404) {
console.error(res.status + ' ' + res.statusText)
}

View File

@@ -407,7 +407,9 @@ app.registerExtension({
if (mediaRecorder) {
try {
mediaRecorder.stop()
} catch {}
} catch {
// Recorder may already be stopped — safe to ignore
}
}
useAudioService().stopAllTracks(currentStream)
currentStream = null

View File

@@ -9,118 +9,62 @@ interface UserCloudStatus {
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
/**
* Helper function to capture API errors with Sentry
*/
function captureApiError(
error: Error,
async function fetchApiWithSentry(
endpoint: string,
errorType: 'http_error' | 'network_error',
httpStatus?: number,
operation?: string,
extraContext?: Record<string, unknown>
) {
const tags: Record<string, string | number> = {
api_endpoint: endpoint,
error_type: errorType
}
if (httpStatus !== undefined) {
tags.http_status = httpStatus
}
if (operation) {
tags.operation = operation
}
const sentryOptions: Sentry.ExclusiveEventHintOrCaptureContext = {
tags,
extra: extraContext ? { ...extraContext } : undefined
}
Sentry.captureException(error, sentryOptions)
}
/**
* Helper function to check if error is already handled HTTP error
*/
function isHttpError(error: unknown, errorMessagePrefix: string): boolean {
return error instanceof Error && error.message.startsWith(errorMessagePrefix)
}
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
init: RequestInit,
operation?: string
): Promise<Response> {
try {
const response = await api.fetchApi('/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const response = await api.fetchApi(endpoint, init)
if (!response.ok) {
const error = new Error(`Failed to get user: ${response.statusText}`)
captureApiError(
error,
'/user',
'http_error',
response.status,
undefined,
{
api: {
method: 'GET',
endpoint: '/user',
status_code: response.status,
status_text: response.statusText
}
}
const error = new Error(
`API ${init.method} ${endpoint} failed: ${response.statusText}`
)
Sentry.captureException(error, {
tags: {
api_endpoint: endpoint,
error_type: 'http_error',
http_status: response.status,
...(operation && { operation })
}
})
throw error
}
return response.json()
return response
} catch (error) {
// Only capture network errors (not HTTP errors we already captured)
if (!isHttpError(error, 'Failed to get user:')) {
captureApiError(error as Error, '/user', 'network_error')
if (!(error instanceof Error) || !error.message.startsWith('API ')) {
Sentry.captureException(error, {
tags: {
api_endpoint: endpoint,
error_type: 'network_error',
...(operation && { operation })
}
})
}
throw error
}
}
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
const response = await fetchApiWithSentry('/user', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
return response.json()
}
export async function getSurveyCompletedStatus(): Promise<boolean> {
try {
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
headers: { 'Content-Type': 'application/json' }
})
if (!response.ok) {
// Not an error case - survey not completed is a valid state
Sentry.addBreadcrumb({
category: 'auth',
message: 'Survey status check returned non-ok response',
level: 'info',
data: {
status: response.status,
endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
}
})
return false
}
if (!response.ok) return false
const data = await response.json()
// Check if data exists and is not empty
return !isEmpty(data.value)
} catch (error) {
// Network error - still capture it as it's not thrown from above
Sentry.captureException(error, {
tags: {
api_endpoint: '/settings/{key}',
error_type: 'network_error'
},
extra: {
route_template: '/settings/{key}',
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
},
tags: { api_endpoint: '/settings/{key}', error_type: 'network_error' },
level: 'warning'
})
return false
@@ -130,59 +74,13 @@ export async function getSurveyCompletedStatus(): Promise<boolean> {
export async function submitSurvey(
survey: Record<string, unknown>
): Promise<void> {
try {
Sentry.addBreadcrumb({
category: 'auth',
message: 'Submitting survey',
level: 'info',
data: {
survey_fields: Object.keys(survey)
}
})
const response = await api.fetchApi('/settings', {
await fetchApiWithSentry(
'/settings',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
})
if (!response.ok) {
const error = new Error(`Failed to submit survey: ${response.statusText}`)
captureApiError(
error,
'/settings',
'http_error',
response.status,
'submit_survey',
{
survey: {
field_count: Object.keys(survey).length,
field_names: Object.keys(survey)
}
}
)
throw error
}
// Log successful survey submission
Sentry.addBreadcrumb({
category: 'auth',
message: 'Survey submitted successfully',
level: 'info'
})
} catch (error) {
// Only capture network errors (not HTTP errors we already captured)
if (!isHttpError(error, 'Failed to submit survey:')) {
captureApiError(
error as Error,
'/settings',
'network_error',
undefined,
'submit_survey'
)
}
throw error
}
},
'submit_survey'
)
}

View File

@@ -153,9 +153,7 @@ export async function extractWorkflow(
const rawWorkflow = parsed.data.extra_data?.extra_pnginfo?.workflow
if (!rawWorkflow) return undefined
const validated = await validateComfyWorkflow(rawWorkflow, (error) => {
console.warn('[extractWorkflow] Workflow validation failed:', error)
})
const validated = await validateComfyWorkflow(rawWorkflow)
return validated ?? undefined
}

View File

@@ -301,7 +301,7 @@ const zBaseExportableGraph = z.object({
})
/** Schema version 0.4 */
export const zComfyWorkflow = zBaseExportableGraph
const zComfyWorkflow = zBaseExportableGraph
.extend({
id: z.string().uuid().optional(),
revision: z.number().optional(),
@@ -353,7 +353,7 @@ interface ComfyWorkflow1BaseOutput extends ComfyWorkflow1BaseType {
}
/** Schema version 1 */
export const zComfyWorkflow1 = zBaseExportableGraph
const zComfyWorkflow1 = zBaseExportableGraph
.extend({
id: z.string().uuid().optional(),
revision: z.number().optional(),

View File

@@ -382,32 +382,26 @@ export class ComfyApp {
}
static copyToClipspace(node: LGraphNode) {
var widgets = null
if (node.widgets) {
widgets = node.widgets.map(({ type, name, value }) => ({
type,
name,
value
}))
}
const widgets = node.widgets
? node.widgets.map(({ type, name, value }) => ({
type,
name,
value
}))
: null
var imgs = undefined
var orig_imgs = undefined
let imgs: HTMLImageElement[] | undefined
let orig_imgs: HTMLImageElement[] | undefined
if (node.imgs != undefined) {
imgs = []
orig_imgs = []
for (let i = 0; i < node.imgs.length; i++) {
imgs[i] = new Image()
imgs[i].src = node.imgs[i].src
orig_imgs[i] = imgs[i]
}
imgs = node.imgs.map((img) => {
const copy = new Image()
copy.src = img.src
return copy
})
orig_imgs = [...imgs]
}
var selectedIndex = 0
if (node.imageIndex) {
selectedIndex = node.imageIndex
}
const selectedIndex = node.imageIndex ?? 0
const paintedIndex = imgs ? imgs.length + 1 : 1
const combinedIndex = imgs ? imgs.length + 2 : 2
@@ -1518,7 +1512,9 @@ export class ComfyApp {
workflow: queuedWorkflow
})
}
} catch (error) {}
} catch (error) {
console.warn('Failed to store execution job:', error)
}
}
} catch (error: unknown) {
if (

View File

@@ -64,13 +64,6 @@ export const useComfyRegistryService = () => {
return `${context}: ${axiosError.message}`
}
/**
* Execute an API request with error and loading state handling
* @param apiCall - Function that returns a promise with the API call
* @param errorContext - Context description for error messages
* @param routeSpecificErrors - Optional map of status codes to custom error messages
* @returns Promise with the API response data or null if the request failed
*/
const executeApiRequest = async <T>(
apiCall: () => Promise<AxiosResponse<T>>,
errorContext: string,
@@ -93,12 +86,6 @@ export const useComfyRegistryService = () => {
}
}
/**
* Get the Comfy Node definitions in a specific version of a node pack
* @param packId - The ID of the node pack
* @param versionId - The version of the node pack
* @returns The node definitions or null if not found or an error occurred
*/
const getNodeDefs = async (
params: {
packId: components['schemas']['Node']['id']
@@ -360,29 +347,6 @@ export const useComfyRegistryService = () => {
)
}
/**
* Get multiple pack versions in a single bulk request.
* This is more efficient than making individual requests for each pack version.
*
* @param nodeVersions - Array of node ID and version pairs to retrieve
* @param signal - Optional AbortSignal for request cancellation
* @returns Bulk response containing the requested node versions or null on error
*
* @example
* ```typescript
* const versions = await getBulkNodeVersions([
* { node_id: 'ComfyUI-Manager', version: '1.0.0' },
* { node_id: 'ComfyUI-Impact-Pack', version: '2.0.0' }
* ])
* if (versions) {
* versions.node_versions.forEach(result => {
* if (result.status === 'success' && result.node_version) {
* console.log(`Retrieved ${result.identifier.node_id}@${result.identifier.version}`)
* }
* })
* }
* ```
*/
const getBulkNodeVersions = async (
nodeVersions: components['schemas']['NodeVersionIdentifier'][],
signal?: AbortSignal

View File

@@ -423,101 +423,91 @@ export const useDialogService = () => {
}
} as const
async function showDeleteWorkspaceDialog(options?: {
async function showWorkspaceDialog(
key: string,
loader: () => Promise<{ default: Component }>,
props?: Record<string, unknown>
) {
const { default: component } = await loader()
return dialogStore.showDialog({
key,
component,
props,
dialogComponentProps: workspaceDialogPt
})
}
function showDeleteWorkspaceDialog(options?: {
workspaceId?: string
workspaceName?: string
}) {
const { default: component } =
await import('@/platform/workspace/components/dialogs/DeleteWorkspaceDialogContent.vue')
return dialogStore.showDialog({
key: 'delete-workspace',
component,
props: options,
dialogComponentProps: workspaceDialogPt
})
return showWorkspaceDialog(
'delete-workspace',
() =>
import('@/platform/workspace/components/dialogs/DeleteWorkspaceDialogContent.vue'),
options
)
}
async function showCreateWorkspaceDialog(
function showCreateWorkspaceDialog(
onConfirm?: (name: string) => void | Promise<void>
) {
const { default: component } =
await import('@/platform/workspace/components/dialogs/CreateWorkspaceDialogContent.vue')
return dialogStore.showDialog({
key: 'create-workspace',
component,
props: { onConfirm },
dialogComponentProps: {
...workspaceDialogPt
}
})
return showWorkspaceDialog(
'create-workspace',
() =>
import('@/platform/workspace/components/dialogs/CreateWorkspaceDialogContent.vue'),
{ onConfirm }
)
}
async function showLeaveWorkspaceDialog() {
const { default: component } =
await import('@/platform/workspace/components/dialogs/LeaveWorkspaceDialogContent.vue')
return dialogStore.showDialog({
key: 'leave-workspace',
component,
dialogComponentProps: workspaceDialogPt
})
function showLeaveWorkspaceDialog() {
return showWorkspaceDialog(
'leave-workspace',
() =>
import('@/platform/workspace/components/dialogs/LeaveWorkspaceDialogContent.vue')
)
}
async function showEditWorkspaceDialog() {
const { default: component } =
await import('@/platform/workspace/components/dialogs/EditWorkspaceDialogContent.vue')
return dialogStore.showDialog({
key: 'edit-workspace',
component,
dialogComponentProps: {
...workspaceDialogPt
}
})
function showEditWorkspaceDialog() {
return showWorkspaceDialog(
'edit-workspace',
() =>
import('@/platform/workspace/components/dialogs/EditWorkspaceDialogContent.vue')
)
}
async function showRemoveMemberDialog(memberId: string) {
const { default: component } =
await import('@/platform/workspace/components/dialogs/RemoveMemberDialogContent.vue')
return dialogStore.showDialog({
key: 'remove-member',
component,
props: { memberId },
dialogComponentProps: workspaceDialogPt
})
function showRemoveMemberDialog(memberId: string) {
return showWorkspaceDialog(
'remove-member',
() =>
import('@/platform/workspace/components/dialogs/RemoveMemberDialogContent.vue'),
{ memberId }
)
}
async function showInviteMemberDialog() {
const { default: component } =
await import('@/platform/workspace/components/dialogs/InviteMemberDialogContent.vue')
return dialogStore.showDialog({
key: 'invite-member',
component,
dialogComponentProps: {
...workspaceDialogPt
}
})
function showInviteMemberDialog() {
return showWorkspaceDialog(
'invite-member',
() =>
import('@/platform/workspace/components/dialogs/InviteMemberDialogContent.vue')
)
}
async function showInviteMemberUpsellDialog() {
const { default: component } =
await import('@/platform/workspace/components/dialogs/InviteMemberUpsellDialogContent.vue')
return dialogStore.showDialog({
key: 'invite-member-upsell',
component,
dialogComponentProps: {
...workspaceDialogPt
}
})
function showInviteMemberUpsellDialog() {
return showWorkspaceDialog(
'invite-member-upsell',
() =>
import('@/platform/workspace/components/dialogs/InviteMemberUpsellDialogContent.vue')
)
}
async function showRevokeInviteDialog(inviteId: string) {
const { default: component } =
await import('@/platform/workspace/components/dialogs/RevokeInviteDialogContent.vue')
return dialogStore.showDialog({
key: 'revoke-invite',
component,
props: { inviteId },
dialogComponentProps: workspaceDialogPt
})
function showRevokeInviteDialog(inviteId: string) {
return showWorkspaceDialog(
'revoke-invite',
() =>
import('@/platform/workspace/components/dialogs/RevokeInviteDialogContent.vue'),
{ inviteId }
)
}
function showBillingComingSoonDialog() {
@@ -538,17 +528,13 @@ export const useDialogService = () => {
})
}
async function showCancelSubscriptionDialog(cancelAt?: string) {
const { default: component } =
await import('@/components/dialog/content/subscription/CancelSubscriptionDialogContent.vue')
return dialogStore.showDialog({
key: 'cancel-subscription',
component,
props: { cancelAt },
dialogComponentProps: {
...workspaceDialogPt
}
})
function showCancelSubscriptionDialog(cancelAt?: string) {
return showWorkspaceDialog(
'cancel-subscription',
() =>
import('@/components/dialog/content/subscription/CancelSubscriptionDialogContent.vue'),
{ cancelAt }
)
}
return {

View File

@@ -9,39 +9,23 @@ import type {
type RegistryNodePack = components['schemas']['Node']
interface ProviderState {
interface NamedProvider {
provider: NodePackSearchProvider
name: string
isHealthy: boolean
lastError?: Error
lastAttempt?: Date
consecutiveFailures: number
}
const CIRCUIT_BREAKER_THRESHOLD = 3 // Number of failures before circuit opens
const CIRCUIT_BREAKER_TIMEOUT = 60000 // 1 minute before retry
/**
* API Gateway for registry search providers with circuit breaker pattern.
* Acts as a single entry point that routes search requests to appropriate providers
* and handles failures gracefully by falling back to alternative providers.
*
* Implements:
* - Gateway pattern: Single entry point for all search requests
* - Circuit breaker: Prevents repeated calls to failed services
* - Automatic failover: Cascades through providers on failure
* Search gateway with primary/fallback provider pattern.
* Tries Algolia first, falls back to ComfyRegistry on failure.
*/
export const useRegistrySearchGateway = (): NodePackSearchProvider => {
const providers: ProviderState[] = []
const providers: NamedProvider[] = []
let activeProviderIndex = 0
// Initialize providers in priority order
try {
providers.push({
provider: useAlgoliaSearchProvider(),
name: 'Algolia',
isHealthy: true,
consecutiveFailures: 0
name: 'Algolia'
})
} catch (error) {
console.warn('Failed to initialize Algolia provider:', error)
@@ -49,170 +33,50 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
providers.push({
provider: useComfyRegistrySearchProvider(),
name: 'ComfyRegistry',
isHealthy: true,
consecutiveFailures: 0
name: 'ComfyRegistry'
})
// TODO: Add an "offline" provider that operates on a local cache of the registry.
/**
* Check if a provider's circuit breaker should be closed (available to try)
*/
const isCircuitClosed = (providerState: ProviderState): boolean => {
if (providerState.consecutiveFailures < CIRCUIT_BREAKER_THRESHOLD) {
return true
}
// Check if enough time has passed to retry
if (providerState.lastAttempt) {
const timeSinceLastAttempt =
Date.now() - providerState.lastAttempt.getTime()
if (timeSinceLastAttempt > CIRCUIT_BREAKER_TIMEOUT) {
return true
}
}
return false
}
/**
* Record a successful call to a provider
*/
const recordSuccess = (providerState: ProviderState) => {
providerState.isHealthy = true
providerState.consecutiveFailures = 0
providerState.lastError = undefined
}
/**
* Record a failed call to a provider
*/
const recordFailure = (providerState: ProviderState, error: Error) => {
providerState.consecutiveFailures++
providerState.lastError = error
providerState.lastAttempt = new Date()
if (providerState.consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
providerState.isHealthy = false
console.warn(
`${providerState.name} provider circuit breaker opened after ${providerState.consecutiveFailures} failures`
)
}
}
/**
* Get the currently active provider based on circuit breaker states
*/
const getActiveProvider = (): NodePackSearchProvider => {
// First, try to use the current active provider if it's healthy
const currentProvider = providers[activeProviderIndex]
if (currentProvider && isCircuitClosed(currentProvider)) {
return currentProvider.provider
}
// Otherwise, find the first healthy provider
for (let i = 0; i < providers.length; i++) {
const providerState = providers[i]
if (isCircuitClosed(providerState)) {
activeProviderIndex = i
return providerState.provider
}
}
throw new Error('No available search providers')
}
/**
* Update the active provider index after a failure.
* Move to the next provider if available.
*/
const updateActiveProviderOnFailure = () => {
if (activeProviderIndex < providers.length - 1) {
activeProviderIndex++
}
}
/**
* Search for node packs.
*/
const searchPacks = async (
query: string,
params: SearchNodePacksParams
): Promise<SearchPacksResult> => {
let lastError: Error | null = null
// Start with the current active provider
for (let attempts = 0; attempts < providers.length; attempts++) {
for (let i = activeProviderIndex; i < providers.length; i++) {
try {
const provider = getActiveProvider()
const providerState = providers[activeProviderIndex]
const result = await provider.searchPacks(query, params)
recordSuccess(providerState)
const result = await providers[i].provider.searchPacks(query, params)
activeProviderIndex = i
return result
} catch (error) {
lastError = error as Error
const providerState = providers[activeProviderIndex]
recordFailure(providerState, lastError)
console.warn(
`${providerState.name} search provider failed (${providerState.consecutiveFailures} failures):`,
error
)
// Try the next provider
updateActiveProviderOnFailure()
console.warn(`${providers[i].name} search failed:`, error)
}
}
// If we get here, all providers failed
throw new Error(
`All search providers failed. Last error: ${lastError?.message || 'Unknown error'}`
`All search providers failed. Last error: ${lastError?.message ?? 'Unknown error'}`
)
}
/**
* Clear the search cache for all providers that implement it.
*/
const clearSearchCache = () => {
for (const providerState of providers) {
for (const { provider, name } of providers) {
try {
providerState.provider.clearSearchCache()
provider.clearSearchCache()
} catch (error) {
console.warn(
`Failed to clear cache for ${providerState.name} provider:`,
error
)
console.warn(`Failed to clear cache for ${name} provider:`, error)
}
}
}
/**
* Get the sort value for a pack.
* @example
* const pack = {
* id: '123',
* name: 'Test Pack',
* downloads: 100
* }
* const sortValue = getSortValue(pack, 'downloads')
* console.log(sortValue) // 100
*/
const getSortValue = (
pack: RegistryNodePack,
sortField: string
): string | number => {
return getActiveProvider().getSortValue(pack, sortField)
return providers[activeProviderIndex].provider.getSortValue(pack, sortField)
}
/**
* Get the sortable fields for the active provider.
* @example
* const sortableFields = getSortableFields()
* console.log(sortableFields) // ['downloads', 'created', 'updated', 'publisher', 'name']
*/
const getSortableFields = () => {
return getActiveProvider().getSortableFields()
return providers[activeProviderIndex].provider.getSortableFields()
}
return {

View File

@@ -82,7 +82,7 @@ interface Load3DNode extends LGraphNode {
const viewerInstances = new Map<NodeId, ReturnType<UseLoad3dViewerFn>>()
export class Load3dService {
class Load3dService {
private static instance: Load3dService
private constructor() {}

View File

@@ -99,83 +99,47 @@ export class ResultItemImpl {
return !!this.format && !!this.frame_rate
}
get extension(): string {
const dotIndex = this.filename.lastIndexOf('.')
return dotIndex >= 0 ? this.filename.slice(dotIndex + 1).toLowerCase() : ''
}
get htmlVideoType(): string | undefined {
if (this.isWebm) {
return 'video/webm'
}
if (this.isMp4) {
return 'video/mp4'
const videoMimeTypes: Record<string, string> = {
webm: 'video/webm',
mp4: 'video/mp4'
}
const byExtension = videoMimeTypes[this.extension]
if (byExtension) return byExtension
if (this.isVhsFormat) {
if (this.format?.endsWith('webm')) {
return 'video/webm'
}
if (this.format?.endsWith('mp4')) {
return 'video/mp4'
for (const [ext, mime] of Object.entries(videoMimeTypes)) {
if (this.format?.endsWith(ext)) return mime
}
}
return undefined
}
get htmlAudioType(): string | undefined {
if (this.isMp3) {
return 'audio/mpeg'
const audioMimeTypes: Record<string, string> = {
mp3: 'audio/mpeg',
wav: 'audio/wav',
ogg: 'audio/ogg',
flac: 'audio/flac'
}
if (this.isWav) {
return 'audio/wav'
}
if (this.isOgg) {
return 'audio/ogg'
}
if (this.isFlac) {
return 'audio/flac'
}
return undefined
}
get isGif(): boolean {
return this.filename.endsWith('.gif')
}
get isWebp(): boolean {
return this.filename.endsWith('.webp')
}
get isWebm(): boolean {
return this.filename.endsWith('.webm')
}
get isMp4(): boolean {
return this.filename.endsWith('.mp4')
return audioMimeTypes[this.extension]
}
get isVideoBySuffix(): boolean {
return this.isWebm || this.isMp4
return this.extension === 'webm' || this.extension === 'mp4'
}
get isImageBySuffix(): boolean {
return this.isGif || this.isWebp
}
get isMp3(): boolean {
return this.filename.endsWith('.mp3')
}
get isWav(): boolean {
return this.filename.endsWith('.wav')
}
get isOgg(): boolean {
return this.filename.endsWith('.ogg')
}
get isFlac(): boolean {
return this.filename.endsWith('.flac')
return this.extension === 'gif' || this.extension === 'webp'
}
get isAudioBySuffix(): boolean {
return this.isMp3 || this.isWav || this.isOgg || this.isFlac
return ['mp3', 'wav', 'ogg', 'flac'].includes(this.extension)
}
get isVideo(): boolean {
@@ -631,11 +595,7 @@ export const useQueuePendingTaskCountStore = defineStore(
}
)
export type AutoQueueMode =
| 'disabled'
| 'change'
| 'instant-idle'
| 'instant-running'
type AutoQueueMode = 'disabled' | 'change' | 'instant-idle' | 'instant-running'
export const isInstantMode = (
mode: AutoQueueMode

View File

@@ -7,7 +7,7 @@ import { isDesktop } from '@/platform/distribution/types'
* Used by desktop-ui app storybook stories
* @public
*/
export type ElectronWindow = typeof window & {
type ElectronWindow = typeof window & {
electronAPI?: ElectronAPI
}

View File

@@ -18,7 +18,7 @@ const MEDIA_SRC_REGEX =
/(<(?:img|source|video)[^>]*\ssrc=['"])(?!(?:\/|https?:\/\/))([^'"\s>]+)(['"])/gi
// Create a marked Renderer that prefixes relative URLs with base
export function createMarkdownRenderer(baseUrl?: string): Renderer {
function createMarkdownRenderer(baseUrl?: string): Renderer {
const normalizedBase = baseUrl ? baseUrl.replace(/\/+$/, '') : ''
const renderer = new Renderer()
renderer.image = ({ href, title, text }) => {