refactor: Migrate the project functionality and optimize the code structure
This commit is contained in:
69
src/hooks/config.ts
Normal file
69
src/hooks/config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useRequest } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export const useConfig = defineStore('config', () => {
|
||||
const mobileDeviceBreakPoint = 759
|
||||
const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint)
|
||||
|
||||
type ModelFolder = Record<string, string[]>
|
||||
const { data: modelFolders, refresh: refreshModelFolders } =
|
||||
useRequest<ModelFolder>('/base-folders')
|
||||
|
||||
const checkDeviceType = () => {
|
||||
isMobile.value = window.innerWidth < mobileDeviceBreakPoint
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', checkDeviceType)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkDeviceType)
|
||||
})
|
||||
|
||||
const refreshSetting = async () => {
|
||||
return Promise.all([refreshModelFolders()])
|
||||
}
|
||||
|
||||
const config = {
|
||||
isMobile,
|
||||
gutter: 16,
|
||||
cardWidth: 240,
|
||||
aspect: 7 / 9,
|
||||
modelFolders,
|
||||
refreshSetting,
|
||||
}
|
||||
|
||||
useAddConfigSettings(config)
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
type Config = ReturnType<typeof useConfig>
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
config: Config
|
||||
}
|
||||
}
|
||||
|
||||
function useAddConfigSettings(config: Config) {
|
||||
onMounted(() => {
|
||||
// API keys
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.APIKey.HuggingFace',
|
||||
name: 'HuggingFace API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.APIKey.Civitai',
|
||||
name: 'Civitai API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
423
src/hooks/download.ts
Normal file
423
src/hooks/download.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { useLoading } from 'hooks/loading'
|
||||
import { MarkdownTool, useMarkdown } from 'hooks/markdown'
|
||||
import { socket } from 'hooks/socket'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { useBoolean } from 'hooks/utils'
|
||||
import { bytesToSize } from 'utils/common'
|
||||
import { onBeforeMount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useDownload = defineStore('download', (store) => {
|
||||
const [visible, toggle] = useBoolean()
|
||||
const { toast, confirm } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const taskList = ref<DownloadTask[]>([])
|
||||
|
||||
const refresh = () => {
|
||||
socket.send('downloadTaskList', null)
|
||||
}
|
||||
|
||||
const createTaskItem = (item: DownloadTaskOptions) => {
|
||||
const { downloadedSize, totalSize, bps, ...rest } = item
|
||||
|
||||
const task: DownloadTask = {
|
||||
...rest,
|
||||
preview: `/model-manager/preview/download/${item.preview}`,
|
||||
downloadProgress: `${bytesToSize(downloadedSize)} / ${bytesToSize(totalSize)}`,
|
||||
downloadSpeed: `${bytesToSize(bps)}/s`,
|
||||
pauseTask() {
|
||||
socket.send('pauseDownloadTask', item.taskId)
|
||||
},
|
||||
resumeTask: () => {
|
||||
socket.send('resumeDownloadTask', item.taskId)
|
||||
},
|
||||
deleteTask: () => {
|
||||
confirm.require({
|
||||
message: t('deleteAsk', [t('downloadTask').toLowerCase()]),
|
||||
header: 'Danger',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: t('cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true,
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('delete'),
|
||||
severity: 'danger',
|
||||
},
|
||||
accept: () => {
|
||||
socket.send('deleteDownloadTask', item.taskId)
|
||||
},
|
||||
reject: () => {},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
socket.addEventListener('reconnected', () => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
socket.addEventListener('downloadTaskList', (event) => {
|
||||
const data = event.detail as DownloadTaskOptions[]
|
||||
|
||||
taskList.value = data.map((item) => {
|
||||
return createTaskItem(item)
|
||||
})
|
||||
})
|
||||
|
||||
socket.addEventListener('createDownloadTask', (event) => {
|
||||
const item = event.detail as DownloadTaskOptions
|
||||
taskList.value.unshift(createTaskItem(item))
|
||||
})
|
||||
|
||||
socket.addEventListener('updateDownloadTask', (event) => {
|
||||
const item = event.detail as DownloadTaskOptions
|
||||
|
||||
for (const task of taskList.value) {
|
||||
if (task.taskId === item.taskId) {
|
||||
if (item.error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: item.error,
|
||||
life: 15000,
|
||||
})
|
||||
item.error = undefined
|
||||
}
|
||||
Object.assign(task, createTaskItem(item))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.addEventListener('deleteDownloadTask', (event) => {
|
||||
const taskId = event.detail as string
|
||||
taskList.value = taskList.value.filter((item) => item.taskId !== taskId)
|
||||
})
|
||||
|
||||
socket.addEventListener('completeDownloadTask', (event) => {
|
||||
const taskId = event.detail as string
|
||||
const task = taskList.value.find((item) => item.taskId === taskId)
|
||||
taskList.value = taskList.value.filter((item) => item.taskId !== taskId)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: `${task?.fullname} Download completed`,
|
||||
life: 2000,
|
||||
})
|
||||
store.models.refresh()
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
return { visible, toggle, data: taskList, refresh }
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
download: ReturnType<typeof useDownload>
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ModelSearch {
|
||||
constructor(readonly md: MarkdownTool) {}
|
||||
|
||||
abstract search(pathname: string): Promise<VersionModel[]>
|
||||
}
|
||||
|
||||
class Civitai extends ModelSearch {
|
||||
async search(searchUrl: string): Promise<VersionModel[]> {
|
||||
const { pathname, searchParams } = new URL(searchUrl)
|
||||
|
||||
const [, modelId] = pathname.match(/^\/models\/(\d*)/) ?? []
|
||||
const versionId = searchParams.get('modelVersionId')
|
||||
|
||||
if (!modelId) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
return fetch(`https://civitai.com/api/v1/models/${modelId}`)
|
||||
.then((response) => response.json())
|
||||
.then((resData) => {
|
||||
const modelVersions: any[] = resData.modelVersions.filter(
|
||||
(version: any) => {
|
||||
if (versionId) {
|
||||
return version.id == versionId
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
const models: VersionModel[] = []
|
||||
|
||||
for (const version of modelVersions) {
|
||||
const modelFiles: any[] = version.files.filter(
|
||||
(file: any) => file.type === 'Model',
|
||||
)
|
||||
|
||||
const shortname = modelFiles.length > 0 ? version.name : undefined
|
||||
|
||||
for (const file of modelFiles) {
|
||||
const fullname = file.name
|
||||
const extension = `.${fullname.split('.').pop()}`
|
||||
const basename = fullname.replace(extension, '')
|
||||
|
||||
models.push({
|
||||
id: file.id,
|
||||
shortname: shortname ?? basename,
|
||||
fullname: fullname,
|
||||
basename: basename,
|
||||
extension: extension,
|
||||
preview: version.images.map((i: any) => i.url),
|
||||
sizeBytes: file.sizeKB * 1024,
|
||||
type: this.resolveType(resData.type),
|
||||
pathIndex: 0,
|
||||
description: [
|
||||
'---',
|
||||
`website: Civitai`,
|
||||
``,
|
||||
`modelPage: https://civitai.com/models/${modelId}?modelVersionId=${version.id}`,
|
||||
'---',
|
||||
'',
|
||||
'# Trigger Words',
|
||||
`\n${(version.trainedWords ?? ['No trigger words']).join(', ')}\n`,
|
||||
'# About this version',
|
||||
this.resolveDescription(
|
||||
version.description,
|
||||
'\nNo description about this version\n',
|
||||
),
|
||||
`# ${resData.name}`,
|
||||
this.resolveDescription(
|
||||
resData.description,
|
||||
'No description about this model',
|
||||
),
|
||||
].join('\n'),
|
||||
metadata: file.metadata,
|
||||
downloadPlatform: 'civitai',
|
||||
downloadUrl: file.downloadUrl,
|
||||
hashes: file.hashes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
})
|
||||
}
|
||||
|
||||
private resolveType(type: string) {
|
||||
const mapLegacy = {
|
||||
TextualInversion: 'embeddings',
|
||||
LoCon: 'loras',
|
||||
DoRA: 'loras',
|
||||
Controlnet: 'controlnet',
|
||||
Upscaler: 'upscale_models',
|
||||
VAE: 'vae',
|
||||
}
|
||||
return mapLegacy[type] ?? `${type.toLowerCase()}s`
|
||||
}
|
||||
|
||||
private resolveDescription(content: string, defaultContent: string) {
|
||||
const mdContent = this.md.parse(content ?? '').trim()
|
||||
return mdContent || defaultContent
|
||||
}
|
||||
}
|
||||
|
||||
class Huggingface extends ModelSearch {
|
||||
async search(searchUrl: string): Promise<VersionModel[]> {
|
||||
const { pathname } = new URL(searchUrl)
|
||||
const [, space, name, ...restPaths] = pathname.split('/')
|
||||
|
||||
if (!space || !name) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
const modelId = `${space}/${name}`
|
||||
const restPathname = restPaths.join('/')
|
||||
|
||||
return fetch(`https://huggingface.co/api/models/${modelId}`)
|
||||
.then((response) => response.json())
|
||||
.then((resData) => {
|
||||
const siblingFiles: string[] = resData.siblings.map(
|
||||
(item: any) => item.rfilename,
|
||||
)
|
||||
|
||||
const modelFiles: string[] = this.filterTreeFiles(
|
||||
this.filterModelFiles(siblingFiles),
|
||||
restPathname,
|
||||
)
|
||||
const images: string[] = this.filterTreeFiles(
|
||||
this.filterImageFiles(siblingFiles),
|
||||
restPathname,
|
||||
).map((filename) => {
|
||||
return `https://huggingface.co/${modelId}/resolve/main/${filename}`
|
||||
})
|
||||
|
||||
const models: VersionModel[] = []
|
||||
|
||||
for (const filename of modelFiles) {
|
||||
const fullname = filename.split('/').pop()!
|
||||
const extension = `.${fullname.split('.').pop()}`
|
||||
const basename = fullname.replace(extension, '')
|
||||
|
||||
models.push({
|
||||
id: filename,
|
||||
shortname: filename,
|
||||
fullname: fullname,
|
||||
basename: basename,
|
||||
extension: extension,
|
||||
preview: images,
|
||||
sizeBytes: 0,
|
||||
type: 'unknown',
|
||||
pathIndex: 0,
|
||||
description: [
|
||||
'---',
|
||||
`website: HuggingFace`,
|
||||
`author: ${resData.author}`,
|
||||
`modelPage: https://huggingface.co/${modelId}`,
|
||||
'---',
|
||||
'',
|
||||
'# Trigger Words',
|
||||
'\nNo trigger words\n',
|
||||
'# About this version',
|
||||
'\nNo description about this version\n',
|
||||
`# ${resData.modelId}`,
|
||||
'\nNo description about this model\n',
|
||||
].join('\n'),
|
||||
metadata: {},
|
||||
downloadPlatform: 'huggingface',
|
||||
downloadUrl: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
|
||||
})
|
||||
}
|
||||
|
||||
return models
|
||||
})
|
||||
}
|
||||
|
||||
private filterTreeFiles(files: string[], pathname: string) {
|
||||
const [target, , ...paths] = pathname.split('/')
|
||||
|
||||
if (!target) return files
|
||||
|
||||
if (target !== 'tree' && target !== 'blob') return files
|
||||
|
||||
const pathPrefix = paths.join('/')
|
||||
return files.filter((file) => {
|
||||
return file.startsWith(pathPrefix)
|
||||
})
|
||||
}
|
||||
|
||||
private filterModelFiles(files: string[]) {
|
||||
const extension = [
|
||||
'.bin',
|
||||
'.ckpt',
|
||||
'.gguf',
|
||||
'.onnx',
|
||||
'.pt',
|
||||
'.pth',
|
||||
'.safetensors',
|
||||
]
|
||||
return files.filter((file) => {
|
||||
const ext = file.split('.').pop()
|
||||
return ext ? extension.includes(`.${ext}`) : false
|
||||
})
|
||||
}
|
||||
|
||||
private filterImageFiles(files: string[]) {
|
||||
const extension = [
|
||||
'.png',
|
||||
'.webp',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.jfif',
|
||||
'.gif',
|
||||
'.apng',
|
||||
]
|
||||
|
||||
return files.filter((file) => {
|
||||
const ext = file.split('.').pop()
|
||||
return ext ? extension.includes(`.${ext}`) : false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownWebsite extends ModelSearch {
|
||||
async search(searchUrl: string): Promise<VersionModel[]> {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'Unknown Website, please input a URL from huggingface.co or civitai.com.',
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelSearch = () => {
|
||||
const loading = useLoading()
|
||||
const md = useMarkdown()
|
||||
const { toast } = useToast()
|
||||
const data = ref<(SelectOptions & { item: VersionModel })[]>([])
|
||||
const current = ref<string | number>()
|
||||
|
||||
const handleSearchByUrl = async (url: string) => {
|
||||
if (!url) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
let instance: ModelSearch = new UnknownWebsite(md)
|
||||
|
||||
const { hostname } = new URL(url ?? '')
|
||||
|
||||
if (hostname === 'civitai.com') {
|
||||
instance = new Civitai(md)
|
||||
}
|
||||
|
||||
if (hostname === 'huggingface.co') {
|
||||
instance = new Huggingface(md)
|
||||
}
|
||||
|
||||
loading.show()
|
||||
return instance
|
||||
.search(url)
|
||||
.then((resData) => {
|
||||
data.value = resData.map((item) => ({
|
||||
label: item.shortname,
|
||||
value: item.id,
|
||||
item,
|
||||
command() {
|
||||
current.value = item.id
|
||||
},
|
||||
}))
|
||||
current.value = data.value[0]?.value
|
||||
|
||||
if (resData.length === 0) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'No Model Found',
|
||||
detail: `No model found for ${url}`,
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
return resData
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: err.message,
|
||||
life: 15000,
|
||||
})
|
||||
return []
|
||||
})
|
||||
.finally(() => loading.hide())
|
||||
}
|
||||
|
||||
return { data, current, search: handleSearchByUrl }
|
||||
}
|
||||
55
src/hooks/loading.ts
Normal file
55
src/hooks/loading.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useBoolean } from 'hooks/utils'
|
||||
import { Ref, ref } from 'vue'
|
||||
|
||||
class GlobalLoading {
|
||||
loading: Ref<boolean>
|
||||
|
||||
loadingStack = 0
|
||||
|
||||
bind(loading: Ref<boolean>) {
|
||||
this.loading = loading
|
||||
}
|
||||
|
||||
show() {
|
||||
this.loadingStack++
|
||||
this.loading.value = true
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.loadingStack--
|
||||
if (this.loadingStack <= 0) this.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export const globalLoading = new GlobalLoading()
|
||||
|
||||
export const useGlobalLoading = defineStore('loading', () => {
|
||||
const [loading] = useBoolean()
|
||||
|
||||
globalLoading.bind(loading)
|
||||
|
||||
return { loading }
|
||||
})
|
||||
|
||||
export const useLoading = () => {
|
||||
const timer = ref<NodeJS.Timeout>()
|
||||
|
||||
const show = () => {
|
||||
timer.value = setTimeout(() => {
|
||||
timer.value = undefined
|
||||
globalLoading.show()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value)
|
||||
timer.value = undefined
|
||||
} else {
|
||||
globalLoading.hide()
|
||||
}
|
||||
}
|
||||
|
||||
return { show, hide }
|
||||
}
|
||||
27
src/hooks/manager.ts
Normal file
27
src/hooks/manager.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useBoolean } from 'hooks/utils'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export const useDialogManager = defineStore('dialogManager', () => {
|
||||
const [visible, toggle] = useBoolean()
|
||||
|
||||
const mounted = ref(false)
|
||||
const open = ref(false)
|
||||
|
||||
watch(visible, (visible) => {
|
||||
open.value = visible
|
||||
mounted.value = true
|
||||
})
|
||||
|
||||
const updateVisible = (val: boolean) => {
|
||||
visible.value = val
|
||||
}
|
||||
|
||||
return { visible: mounted, open, updateVisible, toggle }
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
dialogManager: ReturnType<typeof useDialogManager>
|
||||
}
|
||||
}
|
||||
49
src/hooks/markdown.ts
Normal file
49
src/hooks/markdown.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import metadata_block from 'markdown-it-metadata-block'
|
||||
import TurndownService from 'turndown'
|
||||
import yaml from 'yaml'
|
||||
|
||||
interface MarkdownOptions {
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export const useMarkdown = (opts?: MarkdownOptions) => {
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
|
||||
md.use(metadata_block, {
|
||||
parseMetadata: yaml.parse,
|
||||
meta: opts?.metadata ?? {},
|
||||
})
|
||||
|
||||
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||
const aIndex = tokens[idx].attrIndex('target')
|
||||
|
||||
if (aIndex < 0) {
|
||||
tokens[idx].attrPush(['target', '_blank'])
|
||||
} else {
|
||||
tokens[idx].attrs![aIndex][1] = '_blank'
|
||||
}
|
||||
|
||||
return self.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
const turndown = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
bulletListMarker: '-',
|
||||
})
|
||||
|
||||
turndown.addRule('paragraph', {
|
||||
filter: 'p',
|
||||
replacement: function (content) {
|
||||
return `\n\n${content}`
|
||||
},
|
||||
})
|
||||
|
||||
return { render: md.render.bind(md), parse: turndown.turndown.bind(turndown) }
|
||||
}
|
||||
|
||||
export type MarkdownTool = ReturnType<typeof useMarkdown>
|
||||
547
src/hooks/model.ts
Normal file
547
src/hooks/model.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
import { useLoading } from 'hooks/loading'
|
||||
import { useMarkdown } from 'hooks/markdown'
|
||||
import { request, useRequest } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common'
|
||||
import { ModelGrid } from 'utils/legacy'
|
||||
import { resolveModelType } from 'utils/model'
|
||||
// import {}
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
InjectionKey,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
toRaw,
|
||||
unref,
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useModels = defineStore('models', () => {
|
||||
const { data, refresh } = useRequest<(Model & { visible?: boolean })[]>(
|
||||
'/models',
|
||||
{ defaultValue: [] },
|
||||
)
|
||||
const { toast, confirm } = useToast()
|
||||
const { t } = useI18n()
|
||||
const loading = useLoading()
|
||||
|
||||
const updateModel = async (model: BaseModel, data: BaseModel) => {
|
||||
const formData = new FormData()
|
||||
|
||||
// Check current preview
|
||||
if (model.preview !== data.preview) {
|
||||
const previewFile = await previewUrlToFile(data.preview as string)
|
||||
formData.append('previewFile', previewFile)
|
||||
}
|
||||
|
||||
// Check current description
|
||||
if (model.description !== data.description) {
|
||||
formData.append('description', data.description)
|
||||
}
|
||||
|
||||
// Check current name and pathIndex
|
||||
if (
|
||||
model.fullname !== data.fullname ||
|
||||
model.pathIndex !== data.pathIndex
|
||||
) {
|
||||
formData.append('type', data.type)
|
||||
formData.append('pathIndex', data.pathIndex.toString())
|
||||
formData.append('fullname', data.fullname)
|
||||
}
|
||||
|
||||
if (formData.keys().next().done) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.show()
|
||||
await request(`/model/${model.type}/${model.pathIndex}/${model.fullname}`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to update model',
|
||||
life: 15000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
loading.hide()
|
||||
})
|
||||
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const deleteModel = async (model: BaseModel) => {
|
||||
return new Promise((resolve) => {
|
||||
confirm.require({
|
||||
message: t('deleteAsk', [t('model').toLowerCase()]),
|
||||
header: 'Danger',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: t('cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true,
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('delete'),
|
||||
severity: 'danger',
|
||||
},
|
||||
accept: () => {
|
||||
loading.show()
|
||||
request(`/model/${model.type}/${model.pathIndex}/${model.fullname}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: `${model.fullname} Deleted`,
|
||||
life: 2000,
|
||||
})
|
||||
return refresh()
|
||||
})
|
||||
.then(() => {
|
||||
resolve(void 0)
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: e.message ?? 'Failed to delete model',
|
||||
life: 15000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
loading.hide()
|
||||
})
|
||||
},
|
||||
reject: () => {},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return { data, refresh, remove: deleteModel, update: updateModel }
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
models: ReturnType<typeof useModels>
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelFormData = (getFormData: () => BaseModel) => {
|
||||
const formData = ref<BaseModel>(getFormData())
|
||||
const modelData = ref<BaseModel>(getFormData())
|
||||
|
||||
type ResetCallback = () => void
|
||||
const resetCallback = ref<ResetCallback[]>([])
|
||||
|
||||
const registerReset = (callback: ResetCallback) => {
|
||||
resetCallback.value.push(callback)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
formData.value = getFormData()
|
||||
modelData.value = getFormData()
|
||||
for (const callback of resetCallback.value) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
type SubmitCallback = (data: BaseModel) => void
|
||||
const submitCallback = ref<SubmitCallback[]>([])
|
||||
|
||||
const registerSubmit = (callback: SubmitCallback) => {
|
||||
submitCallback.value.push(callback)
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
const data = cloneDeep(toRaw(unref(formData)))
|
||||
for (const callback of submitCallback.value) {
|
||||
callback(data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const metadata = ref<Record<string, any>>({})
|
||||
|
||||
return {
|
||||
formData,
|
||||
modelData,
|
||||
registerReset,
|
||||
reset,
|
||||
registerSubmit,
|
||||
submit,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
type ModelFormInstance = ReturnType<typeof useModelFormData>
|
||||
|
||||
/**
|
||||
* Model base info
|
||||
*/
|
||||
const baseInfoKey = Symbol('baseInfo') as InjectionKey<
|
||||
ReturnType<typeof useModelBaseInfoEditor>
|
||||
>
|
||||
|
||||
export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => {
|
||||
const { formData: model, modelData } = formInstance
|
||||
|
||||
const type = computed({
|
||||
get: () => {
|
||||
return model.value.type
|
||||
},
|
||||
set: (val) => {
|
||||
model.value.type = val
|
||||
},
|
||||
})
|
||||
|
||||
const pathIndex = computed({
|
||||
get: () => {
|
||||
return model.value.pathIndex
|
||||
},
|
||||
set: (val) => {
|
||||
model.value.pathIndex = val
|
||||
},
|
||||
})
|
||||
|
||||
const extension = computed(() => {
|
||||
return model.value.extension
|
||||
})
|
||||
|
||||
const basename = computed({
|
||||
get: () => {
|
||||
return model.value.fullname.replace(model.value.extension, '')
|
||||
},
|
||||
set: (val) => {
|
||||
model.value.fullname = `${val ?? ''}${model.value.extension}`
|
||||
},
|
||||
})
|
||||
|
||||
interface BaseInfoItem {
|
||||
key: string
|
||||
display: string
|
||||
value: any
|
||||
}
|
||||
|
||||
interface FieldsItem {
|
||||
key: keyof Model
|
||||
formatter: (val: any) => string
|
||||
}
|
||||
|
||||
const baseInfo = computed(() => {
|
||||
const fields: FieldsItem[] = [
|
||||
{
|
||||
key: 'type',
|
||||
formatter: () => resolveModelType(modelData.value.type).display,
|
||||
},
|
||||
{
|
||||
key: 'fullname',
|
||||
formatter: (val) => val,
|
||||
},
|
||||
{
|
||||
key: 'sizeBytes',
|
||||
formatter: (val) => (val == 0 ? 'Unknown' : bytesToSize(val)),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
formatter: (val) => val && formatDate(val),
|
||||
},
|
||||
{
|
||||
key: 'updatedAt',
|
||||
formatter: (val) => val && formatDate(val),
|
||||
},
|
||||
]
|
||||
|
||||
const information: Record<string, BaseInfoItem> = {}
|
||||
for (const item of fields) {
|
||||
const key = item.key
|
||||
const value = model.value[key]
|
||||
const display = item.formatter(value)
|
||||
|
||||
if (display) {
|
||||
information[key] = { key, value, display }
|
||||
}
|
||||
}
|
||||
|
||||
return information
|
||||
})
|
||||
|
||||
const result = {
|
||||
type,
|
||||
baseInfo,
|
||||
basename,
|
||||
extension,
|
||||
pathIndex,
|
||||
}
|
||||
|
||||
provide(baseInfoKey, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const useModelBaseInfo = () => {
|
||||
return inject(baseInfoKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Editable preview image.
|
||||
*
|
||||
* In edit mode, there are 4 methods for setting a preview picture:
|
||||
* 1. default value, which is the default image of the model type
|
||||
* 2. network picture
|
||||
* 3. local file
|
||||
* 4. no preview
|
||||
*/
|
||||
const previewKey = Symbol('preview') as InjectionKey<
|
||||
ReturnType<typeof useModelPreviewEditor>
|
||||
>
|
||||
|
||||
export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
||||
const { formData: model, registerReset, registerSubmit } = formInstance
|
||||
|
||||
const typeOptions = ref(['default', 'network', 'local', 'none'])
|
||||
const currentType = ref('default')
|
||||
|
||||
/**
|
||||
* Default images
|
||||
*/
|
||||
const defaultContent = computed(() => {
|
||||
return Array.isArray(model.value.preview)
|
||||
? model.value.preview
|
||||
: [model.value.preview]
|
||||
})
|
||||
const defaultContentPage = ref(0)
|
||||
|
||||
/**
|
||||
* Network picture url
|
||||
*/
|
||||
const networkContent = ref<string>()
|
||||
|
||||
/**
|
||||
* Local file url
|
||||
*/
|
||||
const localContent = ref<string>()
|
||||
const updateLocalContent = async (event: SelectEvent) => {
|
||||
const { files } = event
|
||||
localContent.value = files[0].objectURL
|
||||
}
|
||||
|
||||
/**
|
||||
* No preview
|
||||
*/
|
||||
const noPreviewContent = computed(() => {
|
||||
return `/model-manager/preview/${model.value.type}/0/no-preview.png`
|
||||
})
|
||||
|
||||
const preview = computed(() => {
|
||||
let content: string | undefined
|
||||
|
||||
switch (currentType.value) {
|
||||
case 'default':
|
||||
content = defaultContent.value[defaultContentPage.value]
|
||||
break
|
||||
case 'network':
|
||||
content = networkContent.value
|
||||
break
|
||||
case 'local':
|
||||
content = localContent.value
|
||||
break
|
||||
default:
|
||||
content = noPreviewContent.value
|
||||
break
|
||||
}
|
||||
|
||||
return content
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
registerReset(() => {
|
||||
currentType.value = 'default'
|
||||
defaultContentPage.value = 0
|
||||
networkContent.value = undefined
|
||||
localContent.value = undefined
|
||||
})
|
||||
|
||||
registerSubmit((data) => {
|
||||
data.preview = preview.value ?? noPreviewContent.value
|
||||
})
|
||||
})
|
||||
|
||||
const result = {
|
||||
preview,
|
||||
typeOptions,
|
||||
currentType,
|
||||
// default value
|
||||
defaultContent,
|
||||
defaultContentPage,
|
||||
// network picture
|
||||
networkContent,
|
||||
// local file
|
||||
localContent,
|
||||
updateLocalContent,
|
||||
// no preview
|
||||
noPreviewContent,
|
||||
}
|
||||
|
||||
provide(previewKey, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const useModelPreview = () => {
|
||||
return inject(previewKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Model description
|
||||
*/
|
||||
const descriptionKey = Symbol('description') as InjectionKey<
|
||||
ReturnType<typeof useModelDescriptionEditor>
|
||||
>
|
||||
|
||||
export const useModelDescriptionEditor = (formInstance: ModelFormInstance) => {
|
||||
const { formData: model, metadata } = formInstance
|
||||
|
||||
const md = useMarkdown({ metadata: metadata.value })
|
||||
|
||||
const description = computed({
|
||||
get: () => {
|
||||
return model.value.description
|
||||
},
|
||||
set: (val) => {
|
||||
model.value.description = val
|
||||
},
|
||||
})
|
||||
|
||||
const renderedDescription = computed(() => {
|
||||
return description.value ? md.render(description.value) : undefined
|
||||
})
|
||||
|
||||
const result = { renderedDescription, description }
|
||||
|
||||
provide(descriptionKey, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const useModelDescription = () => {
|
||||
return inject(descriptionKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Model metadata
|
||||
*/
|
||||
const metadataKey = Symbol('metadata') as InjectionKey<
|
||||
ReturnType<typeof useModelMetadataEditor>
|
||||
>
|
||||
|
||||
export const useModelMetadataEditor = (formInstance: ModelFormInstance) => {
|
||||
const { formData: model } = formInstance
|
||||
|
||||
const metadata = computed(() => {
|
||||
return model.value.metadata
|
||||
})
|
||||
|
||||
const result = { metadata }
|
||||
|
||||
provide(metadataKey, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const useModelMetadata = () => {
|
||||
return inject(metadataKey)!
|
||||
}
|
||||
|
||||
export const useModelNodeAction = (model: BaseModel) => {
|
||||
const { t } = useI18n()
|
||||
const { toast, wrapperToastError } = useToast()
|
||||
|
||||
const createNode = (options: Record<string, any> = {}) => {
|
||||
const nodeType = resolveModelType(model.type).loader
|
||||
if (!nodeType) {
|
||||
throw new Error(t('unSupportedModelType', [model.type]))
|
||||
}
|
||||
|
||||
const node = window.LiteGraph.createNode(nodeType, null, options)
|
||||
const widgetIndex = node.widgets.findIndex((w) => w.type === 'combo')
|
||||
if (widgetIndex > -1) {
|
||||
node.widgets[widgetIndex].value = model.fullname
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
const dragToAddModelNode = wrapperToastError((event: DragEvent) => {
|
||||
// const target = document.elementFromPoint(event.clientX, event.clientY)
|
||||
// if (
|
||||
// target?.tagName.toLocaleLowerCase() === 'canvas' &&
|
||||
// target.id === 'graph-canvas'
|
||||
// ) {
|
||||
// const pos = app.clientPosToCanvasPos([event.clientX - 20, event.clientY])
|
||||
// const node = createNode({ pos })
|
||||
// app.graph.add(node)
|
||||
// app.canvas.selectNode(node)
|
||||
// }
|
||||
//
|
||||
// Use the legacy method instead
|
||||
const removeEmbeddingExtension = true
|
||||
const strictDragToAdd = false
|
||||
|
||||
ModelGrid.dragAddModel(
|
||||
event,
|
||||
model.type,
|
||||
model.fullname,
|
||||
removeEmbeddingExtension,
|
||||
strictDragToAdd,
|
||||
)
|
||||
})
|
||||
|
||||
const addModelNode = wrapperToastError(() => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
const firstSelectedNode = Object.values(selectedNodes)[0]
|
||||
const offset = 25
|
||||
const pos = firstSelectedNode
|
||||
? [firstSelectedNode.pos[0] + offset, firstSelectedNode.pos[1] + offset]
|
||||
: app.canvas.canvas_mouse
|
||||
const node = createNode({ pos })
|
||||
app.graph.add(node)
|
||||
app.canvas.selectNode(node)
|
||||
})
|
||||
|
||||
const copyModelNode = wrapperToastError(() => {
|
||||
const node = createNode()
|
||||
app.canvas.copyToClipboard([node])
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: t('modelCopied'),
|
||||
life: 2000,
|
||||
})
|
||||
})
|
||||
|
||||
const loadPreviewWorkflow = wrapperToastError(async () => {
|
||||
const previewUrl = model.preview as string
|
||||
const response = await fetch(previewUrl)
|
||||
const data = await response.blob()
|
||||
const type = data.type
|
||||
const extension = type.split('/').pop()
|
||||
const file = new File([data], `${model.fullname}.${extension}`, { type })
|
||||
app.handleFile(file)
|
||||
})
|
||||
|
||||
return {
|
||||
addModelNode,
|
||||
dragToAddModelNode,
|
||||
copyModelNode,
|
||||
loadPreviewWorkflow,
|
||||
}
|
||||
}
|
||||
85
src/hooks/request.ts
Normal file
85
src/hooks/request.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useLoading } from 'hooks/loading'
|
||||
import { api } from 'scripts/comfyAPI'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
export const request = async (url: string, options?: RequestInit) => {
|
||||
return api
|
||||
.fetchApi(`/model-manager${url}`, options)
|
||||
.then((response) => response.json())
|
||||
.then((resData) => {
|
||||
if (resData.success) {
|
||||
return resData.data
|
||||
}
|
||||
throw new Error(resData.error)
|
||||
})
|
||||
}
|
||||
|
||||
export interface RequestOptions<T> {
|
||||
method?: RequestInit['method']
|
||||
headers?: RequestInit['headers']
|
||||
defaultParams?: Record<string, any>
|
||||
defaultValue?: any
|
||||
postData?: (data: T) => T
|
||||
manual?: boolean
|
||||
}
|
||||
|
||||
export const useRequest = <T = any>(
|
||||
url: string,
|
||||
options: RequestOptions<T> = {},
|
||||
) => {
|
||||
const loading = useLoading()
|
||||
const postData = options.postData ?? ((data) => data)
|
||||
|
||||
const data = ref<T>(options.defaultValue)
|
||||
const lastParams = ref()
|
||||
|
||||
const fetch = async (
|
||||
params: Record<string, any> = options.defaultParams ?? {},
|
||||
) => {
|
||||
loading.show()
|
||||
|
||||
lastParams.value = params
|
||||
|
||||
let requestUrl = url
|
||||
const requestOptions: RequestInit = {
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
}
|
||||
const requestParams = { ...params }
|
||||
|
||||
const templatePattern = /\{(.*?)\}/g
|
||||
const urlParamKeyMatches = requestUrl.matchAll(templatePattern)
|
||||
for (const urlParamKey of urlParamKeyMatches) {
|
||||
const [match, paramKey] = urlParamKey
|
||||
if (paramKey in requestParams) {
|
||||
const paramValue = requestParams[paramKey]
|
||||
delete requestParams[paramKey]
|
||||
requestUrl = requestUrl.replace(match, paramValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestOptions.method) {
|
||||
requestOptions.method = 'GET'
|
||||
}
|
||||
|
||||
if (requestOptions.method !== 'GET') {
|
||||
requestOptions.body = JSON.stringify(requestParams)
|
||||
}
|
||||
|
||||
return request(requestUrl, requestOptions)
|
||||
.then((resData) => (data.value = postData(resData)))
|
||||
.finally(() => loading.hide())
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!options.manual) {
|
||||
fetch()
|
||||
}
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
return fetch(lastParams.value)
|
||||
}
|
||||
|
||||
return { data, refresh, fetch }
|
||||
}
|
||||
22
src/hooks/resize.ts
Normal file
22
src/hooks/resize.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { throttle } from 'lodash'
|
||||
import { Directive } from 'vue'
|
||||
|
||||
export const resizeDirective: Directive<HTMLElement, ResizeObserverCallback> = {
|
||||
mounted: (el, binding) => {
|
||||
const callback = binding.value ?? (() => {})
|
||||
const observer = new ResizeObserver(callback)
|
||||
observer.observe(el)
|
||||
el['observer'] = observer
|
||||
},
|
||||
unmounted: (el) => {
|
||||
const observer = el['observer']
|
||||
observer.disconnect()
|
||||
},
|
||||
}
|
||||
|
||||
export const defineResizeCallback = (
|
||||
callback: ResizeObserverCallback,
|
||||
wait?: number,
|
||||
) => {
|
||||
return throttle(callback, wait ?? 100)
|
||||
}
|
||||
82
src/hooks/socket.ts
Normal file
82
src/hooks/socket.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { globalToast } from 'hooks/toast'
|
||||
import { readonly } from 'vue'
|
||||
|
||||
class WebSocketEvent extends EventTarget {
|
||||
private socket: WebSocket | null
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.createSocket()
|
||||
}
|
||||
|
||||
private createSocket(isReconnect?: boolean) {
|
||||
const api_host = location.host
|
||||
const api_base = location.pathname.split('/').slice(0, -1).join('/')
|
||||
|
||||
let opened = false
|
||||
let existingSession = window.name
|
||||
if (existingSession) {
|
||||
existingSession = '?clientId=' + existingSession
|
||||
}
|
||||
|
||||
this.socket = readonly(
|
||||
new WebSocket(
|
||||
`ws${window.location.protocol === 'https:' ? 's' : ''}://${api_host}${api_base}/model-manager/ws${existingSession}`,
|
||||
),
|
||||
)
|
||||
|
||||
this.socket.addEventListener('open', () => {
|
||||
opened = true
|
||||
if (isReconnect) {
|
||||
this.dispatchEvent(new CustomEvent('reconnected'))
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.addEventListener('error', () => {
|
||||
if (this.socket) this.socket.close()
|
||||
})
|
||||
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
setTimeout(() => {
|
||||
this.socket = null
|
||||
this.createSocket(true)
|
||||
}, 300)
|
||||
if (opened) {
|
||||
this.dispatchEvent(new CustomEvent('status', { detail: null }))
|
||||
this.dispatchEvent(new CustomEvent('reconnecting'))
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'error') {
|
||||
globalToast.value?.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: msg.data,
|
||||
life: 15000,
|
||||
})
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addEventListener = (
|
||||
type: string,
|
||||
callback: CustomEventListener | null,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
) => {
|
||||
super.addEventListener(type, callback, options)
|
||||
}
|
||||
|
||||
send(type: string, data: any) {
|
||||
this.socket?.send(JSON.stringify({ type, detail: data }))
|
||||
}
|
||||
}
|
||||
|
||||
export const socket = new WebSocketEvent()
|
||||
51
src/hooks/store.ts
Normal file
51
src/hooks/store.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { inject, InjectionKey, provide } from 'vue'
|
||||
|
||||
const providerHooks = new Map<string, any>()
|
||||
const storeEvent = {} as StoreProvider
|
||||
|
||||
export const useStoreProvider = () => {
|
||||
// const storeEvent = {}
|
||||
|
||||
for (const [key, useHook] of providerHooks) {
|
||||
storeEvent[key] = useHook()
|
||||
}
|
||||
|
||||
return storeEvent
|
||||
}
|
||||
|
||||
const storeKeys = new Map<string, Symbol>()
|
||||
|
||||
const getStoreKey = (key: string) => {
|
||||
let storeKey = storeKeys.get(key)
|
||||
if (!storeKey) {
|
||||
storeKey = Symbol(key)
|
||||
storeKeys.set(key, storeKey)
|
||||
}
|
||||
return storeKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Using vue provide and inject to implement a simple store
|
||||
*/
|
||||
export const defineStore = <T = any>(
|
||||
key: string,
|
||||
useInitial: (event: StoreProvider) => T,
|
||||
) => {
|
||||
const storeKey = getStoreKey(key) as InjectionKey<T>
|
||||
|
||||
if (providerHooks.has(key) && !import.meta.hot) {
|
||||
console.warn(`[defineStore] key: ${key} already exists.`)
|
||||
} else {
|
||||
providerHooks.set(key, () => {
|
||||
const result = useInitial(storeEvent)
|
||||
provide(storeKey, result ?? storeEvent[key])
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const useStore = () => {
|
||||
return inject(storeKey)!
|
||||
}
|
||||
|
||||
return useStore
|
||||
}
|
||||
45
src/hooks/toast.ts
Normal file
45
src/hooks/toast.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ToastServiceMethods } from 'primevue/toastservice'
|
||||
import { useConfirm as usePrimeConfirm } from 'primevue/useconfirm'
|
||||
import { useToast as usePrimeToast } from 'primevue/usetoast'
|
||||
|
||||
export const globalToast = { value: null } as unknown as {
|
||||
value: ToastServiceMethods
|
||||
}
|
||||
|
||||
export const useToast = () => {
|
||||
const toast = usePrimeToast()
|
||||
const confirm = usePrimeConfirm()
|
||||
|
||||
globalToast.value = toast
|
||||
|
||||
const wrapperToastError = <T extends Function>(callback: T): T => {
|
||||
const showToast = (error: Error) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 15000,
|
||||
})
|
||||
}
|
||||
|
||||
const isAsync = callback.constructor.name === 'AsyncFunction'
|
||||
|
||||
let wrapperExec: any
|
||||
|
||||
if (isAsync) {
|
||||
wrapperExec = (...args: any[]) => callback(...args).catch(showToast)
|
||||
} else {
|
||||
wrapperExec = (...args: any[]) => {
|
||||
try {
|
||||
return callback(...args)
|
||||
} catch (error) {
|
||||
showToast(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wrapperExec
|
||||
}
|
||||
|
||||
return { toast, wrapperToastError, confirm }
|
||||
}
|
||||
11
src/hooks/utils.ts
Normal file
11
src/hooks/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useBoolean = (defaultValue?: boolean) => {
|
||||
const target = ref(defaultValue ?? false)
|
||||
|
||||
const toggle = (value?: any) => {
|
||||
target.value = typeof value === 'boolean' ? value : !target.value
|
||||
}
|
||||
|
||||
return [target, toggle] as const
|
||||
}
|
||||
Reference in New Issue
Block a user