Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UV mirrors settings #2333

Merged
merged 17 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/comfyui-electron-types": "^0.4.16",
"@comfyorg/litegraph": "^0.8.62",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
Expand Down
38 changes: 19 additions & 19 deletions src/components/common/UrlInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
v-bind="$attrs"
:model-value="internalValue"
class="w-full"
:invalid="validationState === UrlValidationState.INVALID"
:invalid="validationState === ValidationState.INVALID"
@update:model-value="handleInput"
@blur="handleBlur"
/>
<InputIcon
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === UrlValidationState.LOADING,
validationState === ValidationState.LOADING,
'pi pi-check text-green-500 cursor-pointer':
validationState === UrlValidationState.VALID,
validationState === ValidationState.VALID,
'pi pi-times text-red-500 cursor-pointer':
validationState === UrlValidationState.INVALID
validationState === ValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
/>
Expand All @@ -30,6 +30,7 @@ import { onMounted, ref, watch } from 'vue'

import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'

const props = defineProps<{
modelValue: string
Expand All @@ -38,16 +39,10 @@ const props = defineProps<{

const emit = defineEmits<{
'update:modelValue': [value: string]
'state-change': [state: ValidationState]
}>()

enum UrlValidationState {
IDLE = 'IDLE',
LOADING = 'LOADING',
VALID = 'VALID',
INVALID = 'INVALID'
}

const validationState = ref<UrlValidationState>(UrlValidationState.IDLE)
const validationState = ref<ValidationState>(ValidationState.IDLE)

// Add internal value state
const internalValue = ref(props.modelValue)
Expand All @@ -60,6 +55,11 @@ watch(
await validateUrl(newValue)
}
)

watch(validationState, (newState) => {
emit('state-change', newState)
})

// Validate on mount
onMounted(async () => {
await validateUrl(props.modelValue)
Expand All @@ -69,7 +69,7 @@ const handleInput = (value: string) => {
// Update internal value without emitting
internalValue.value = value
// Reset validation state when user types
validationState.value = UrlValidationState.IDLE
validationState.value = ValidationState.IDLE
}

const handleBlur = async () => {
Expand All @@ -88,24 +88,24 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
}

const validateUrl = async (value: string) => {
if (validationState.value === UrlValidationState.LOADING) return
if (validationState.value === ValidationState.LOADING) return

const url = value.trim()

// Reset state
validationState.value = UrlValidationState.IDLE
validationState.value = ValidationState.IDLE

// Skip validation if empty
if (!url) return

validationState.value = UrlValidationState.LOADING
validationState.value = ValidationState.LOADING
try {
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
validationState.value = isValid
? UrlValidationState.VALID
: UrlValidationState.INVALID
? ValidationState.VALID
: ValidationState.INVALID
} catch {
validationState.value = UrlValidationState.INVALID
validationState.value = ValidationState.INVALID
}
}

Expand Down
70 changes: 70 additions & 0 deletions src/components/install/MirrorsConfiguration.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>
<Panel
:header="$t('install.settings.mirrorSettings')"
toggleable
:collapsed="!showMirrorInputs"
pt:root="bg-neutral-800 border-none w-[600px]"
>
<template
v-for="([item, modelValue], index) in mirrors"
:key="item.settingId"
>
<Divider v-if="index > 0" />

<MirrorItem
:item="item"
v-model="modelValue.value"
@state-change="validationStates[index] = $event"
/>
</template>
<template #icons>
<i
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === ValidationState.LOADING,
'pi pi-check text-green-500':
validationState === ValidationState.VALID,
'pi pi-times text-red-500':
validationState === ValidationState.INVALID
}"
v-tooltip="validationStateTooltip"
/>
</template>
</Panel>
</template>

<script setup lang="ts">
import _ from 'lodash'
import Divider from 'primevue/divider'
import Panel from 'primevue/panel'
import { computed, ref } from 'vue'

import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import { UV_MIRRORS } from '@/constants/uvMirrors'
import { t } from '@/i18n'
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'

const showMirrorInputs = ref(false)
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
const torchMirror = defineModel<string>('torchMirror', { required: true })

const mirrors = _.zip(UV_MIRRORS, [pythonMirror, pypiMirror, torchMirror])

const validationStates = ref<ValidationState[]>(
mirrors.map(() => ValidationState.IDLE)
)
const validationState = computed(() => {
return mergeValidationStates(validationStates.value)
})
const validationStateTooltip = computed(() => {
switch (validationState.value) {
case ValidationState.INVALID:
return t('install.settings.mirrorsUnreachable')
case ValidationState.VALID:
return t('install.settings.mirrorsReachable')
default:
return t('install.settings.checkingMirrors')
}
})
</script>
66 changes: 66 additions & 0 deletions src/components/install/mirror/MirrorItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="w-full">
<h3 class="text-lg font-medium text-neutral-100">
{{ $t(`settings.${normalizedSettingId}.name`) }}
</h3>
<p class="text-sm text-neutral-400 mt-1">
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
</p>
</div>
<UrlInput
v-model="modelValue"
:validate-url-fn="checkMirrorReachable"
@state-change="validationState = $event"
/>
</div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'

import { UVMirror } from '@/constants/uvMirrors'
import { electronAPI } from '@/utils/envUtil'
import { isValidUrl, normalizeI18nKey } from '@/utils/formatUtil'
import { ValidationState } from '@/utils/validationUtil'

const { item } = defineProps<{
item: UVMirror
}>()

const emit = defineEmits<{
'state-change': [state: ValidationState]
}>()

const modelValue = defineModel<string>('modelValue', { required: true })
const validationState = ref<ValidationState>(ValidationState.IDLE)

const normalizedSettingId = computed(() => {
return normalizeI18nKey(item.settingId)
})

const checkMirrorReachable = async (mirror: string) => {
return (
isValidUrl(mirror) &&
(await electronAPI().NetWork.canAccessUrl(
mirror + (item.validationPathSuffix ?? '')
))
)
}

onMounted(() => {
modelValue.value = item.mirror
})

watch(validationState, (newState) => {
emit('state-change', newState)

// Set fallback mirror if default mirror is invalid
if (
newState === ValidationState.INVALID &&
modelValue.value === item.mirror
) {
modelValue.value = item.fallbackMirror
}
})
</script>
51 changes: 51 additions & 0 deletions src/constants/uvMirrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CUDA_TORCH_URL } from '@comfyorg/comfyui-electron-types'
import { NIGHTLY_CPU_TORCH_URL } from '@comfyorg/comfyui-electron-types'

import { electronAPI, isElectron } from '@/utils/envUtil'

export interface UVMirror {
/**
* The setting id defined for the mirror.
*/
settingId: string
/**
* The default mirror to use.
*/
mirror: string
/**
* The fallback mirror to use.
*/
fallbackMirror: string
/**
* The path suffix to validate the mirror is reachable.
*/
validationPathSuffix?: string
}

const DEFAULT_TORCH_MIRROR = isElectron()
? electronAPI().getPlatform() === 'darwin'
? NIGHTLY_CPU_TORCH_URL
: CUDA_TORCH_URL
: ''

export const UV_MIRRORS: UVMirror[] = [
{
settingId: 'Comfy-Desktop.UV.PythonInstallMirror',
mirror:
'https://github.com/astral-sh/python-build-standalone/releases/download',
fallbackMirror:
'https://ghfast.top/https://github.com/astral-sh/python-build-standalone/releases/download',
validationPathSuffix:
'/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'
},
{
settingId: 'Comfy-Desktop.UV.PypiInstallMirror',
mirror: 'https://pypi.org/simple/',
fallbackMirror: 'https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple'
},
{
settingId: 'Comfy-Desktop.UV.TorchInstallMirror',
mirror: DEFAULT_TORCH_MIRROR,
fallbackMirror: DEFAULT_TORCH_MIRROR
}
]
22 changes: 21 additions & 1 deletion src/extensions/core/electronAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { t } from '@/i18n'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'

Expand Down Expand Up @@ -55,6 +54,27 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'

electronAPI.Config.setWindowStyle(newValue)
}
},
{
id: 'Comfy-Desktop.UV.PythonInstallMirror',
name: 'Python Install Mirror',
tooltip: `Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme.`,
type: 'url',
defaultValue: ''
},
{
id: 'Comfy-Desktop.UV.PypiInstallMirror',
name: 'Pypi Install Mirror',
tooltip: `Default pip install mirror`,
type: 'url',
defaultValue: ''
},
{
id: 'Comfy-Desktop.UV.TorchInstallMirror',
name: 'Torch Install Mirror',
tooltip: `Pip install mirror for pytorch`,
type: 'url',
defaultValue: ''
}
],

Expand Down
9 changes: 8 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,13 @@
"customNodeConfigurations": "Custom node configurations"
},
"viewFullPolicy": "View full policy"
}
},
"pythonMirrorPlaceholder": "Enter Python mirror URL",
"pypiMirrorPlaceholder": "Enter PyPI mirror URL",
"checkingMirrors": "Checking network access to python mirrors...",
"mirrorsReachable": "Network access to python mirrors is good",
"mirrorsUnreachable": "Network access to some python mirrors is bad",
"mirrorSettings": "Mirror Settings"
},
"customNodes": "Custom Nodes",
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations.",
Expand Down Expand Up @@ -472,6 +478,7 @@
"About": "About",
"EditTokenWeight": "Edit Token Weight",
"CustomColorPalettes": "Custom Color Palettes",
"UV": "UV",
"ContextMenu": "Context Menu"
},
"serverConfigItems": {
Expand Down
Loading