Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811f1bc352 | ||
|
|
5342b7ec92 | ||
|
|
30e1714397 | ||
|
|
384a106917 | ||
|
|
7378a7deae | ||
|
|
1975e2056d |
@@ -1,14 +1,22 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import math
|
||||||
import yaml
|
import yaml
|
||||||
import requests
|
import requests
|
||||||
import markdownify
|
import markdownify
|
||||||
|
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
|
||||||
class ModelSearcher(ABC):
|
class ModelSearcher(ABC):
|
||||||
@@ -282,25 +290,6 @@ class HuggingfaceModelSearcher(ModelSearcher):
|
|||||||
return _filter_tree_files
|
return _filter_tree_files
|
||||||
|
|
||||||
|
|
||||||
def get_model_searcher_by_url(url: str) -> ModelSearcher:
|
|
||||||
parsed_url = urlparse(url)
|
|
||||||
host_name = parsed_url.hostname
|
|
||||||
if host_name == "civitai.com":
|
|
||||||
return CivitaiModelSearcher()
|
|
||||||
elif host_name == "huggingface.co":
|
|
||||||
return HuggingfaceModelSearcher()
|
|
||||||
return UnknownWebsiteSearcher()
|
|
||||||
|
|
||||||
|
|
||||||
import folder_paths
|
|
||||||
|
|
||||||
|
|
||||||
from . import config
|
|
||||||
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
|
|
||||||
class Information:
|
class Information:
|
||||||
def add_routes(self, routes):
|
def add_routes(self, routes):
|
||||||
|
|
||||||
@@ -347,18 +336,30 @@ class Information:
|
|||||||
index = int(request.match_info.get("index", None))
|
index = int(request.match_info.get("index", None))
|
||||||
filename = request.match_info.get("filename", None)
|
filename = request.match_info.get("filename", None)
|
||||||
|
|
||||||
|
content_type = utils.resolve_file_content_type(filename)
|
||||||
|
|
||||||
|
if content_type == "video":
|
||||||
|
abs_path = utils.get_full_path(model_type, index, filename)
|
||||||
|
return web.FileResponse(abs_path)
|
||||||
|
|
||||||
extension_uri = config.extension_uri
|
extension_uri = config.extension_uri
|
||||||
|
|
||||||
try:
|
try:
|
||||||
folders = folder_paths.get_folder_paths(model_type)
|
folders = folder_paths.get_folder_paths(model_type)
|
||||||
base_path = folders[index]
|
base_path = folders[index]
|
||||||
abs_path = utils.join_path(base_path, filename)
|
abs_path = utils.join_path(base_path, filename)
|
||||||
|
preview_name = utils.get_model_preview_name(abs_path)
|
||||||
|
if preview_name:
|
||||||
|
dir_name = os.path.dirname(abs_path)
|
||||||
|
abs_path = utils.join_path(dir_name, preview_name)
|
||||||
except:
|
except:
|
||||||
abs_path = extension_uri
|
abs_path = extension_uri
|
||||||
|
|
||||||
if not os.path.isfile(abs_path):
|
if not os.path.isfile(abs_path):
|
||||||
abs_path = utils.join_path(extension_uri, "assets", "no-preview.png")
|
abs_path = utils.join_path(extension_uri, "assets", "no-preview.png")
|
||||||
return web.FileResponse(abs_path)
|
|
||||||
|
image_data = self.get_image_preview_data(abs_path)
|
||||||
|
return web.Response(body=image_data.getvalue(), content_type="image/webp")
|
||||||
|
|
||||||
@routes.get("/model-manager/preview/download/{filename}")
|
@routes.get("/model-manager/preview/download/{filename}")
|
||||||
async def read_download_preview(request):
|
async def read_download_preview(request):
|
||||||
@@ -373,11 +374,69 @@ class Information:
|
|||||||
|
|
||||||
return web.FileResponse(preview_path)
|
return web.FileResponse(preview_path)
|
||||||
|
|
||||||
|
def get_image_preview_data(self, filename: str):
|
||||||
|
with Image.open(filename) as img:
|
||||||
|
max_size = 1024
|
||||||
|
original_format = img.format
|
||||||
|
|
||||||
|
exif_data = img.info.get("exif")
|
||||||
|
icc_profile = img.info.get("icc_profile")
|
||||||
|
|
||||||
|
if getattr(img, "is_animated", False) and img.n_frames > 1:
|
||||||
|
total_frames = img.n_frames
|
||||||
|
step = max(1, math.ceil(total_frames / 30))
|
||||||
|
|
||||||
|
frames, durations = [], []
|
||||||
|
|
||||||
|
for frame_idx in range(0, total_frames, step):
|
||||||
|
img.seek(frame_idx)
|
||||||
|
frame = img.copy()
|
||||||
|
frame.thumbnail((max_size, max_size), Image.Resampling.NEAREST)
|
||||||
|
|
||||||
|
frames.append(frame)
|
||||||
|
durations.append(img.info.get("duration", 100) * step)
|
||||||
|
|
||||||
|
save_args = {
|
||||||
|
"format": "WEBP",
|
||||||
|
"save_all": True,
|
||||||
|
"append_images": frames[1:],
|
||||||
|
"duration": durations,
|
||||||
|
"loop": 0,
|
||||||
|
"quality": 80,
|
||||||
|
"method": 0,
|
||||||
|
"allow_mixed": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exif_data:
|
||||||
|
save_args["exif"] = exif_data
|
||||||
|
|
||||||
|
if icc_profile:
|
||||||
|
save_args["icc_profile"] = icc_profile
|
||||||
|
|
||||||
|
img_byte_arr = BytesIO()
|
||||||
|
frames[0].save(img_byte_arr, **save_args)
|
||||||
|
img_byte_arr.seek(0)
|
||||||
|
return img_byte_arr
|
||||||
|
|
||||||
|
img.thumbnail((max_size, max_size), Image.Resampling.BICUBIC)
|
||||||
|
|
||||||
|
img_byte_arr = BytesIO()
|
||||||
|
save_args = {"format": "WEBP", "quality": 80}
|
||||||
|
|
||||||
|
if exif_data:
|
||||||
|
save_args["exif"] = exif_data
|
||||||
|
if icc_profile:
|
||||||
|
save_args["icc_profile"] = icc_profile
|
||||||
|
|
||||||
|
img.save(img_byte_arr, **save_args)
|
||||||
|
img_byte_arr.seek(0)
|
||||||
|
return img_byte_arr
|
||||||
|
|
||||||
def fetch_model_info(self, model_page: str):
|
def fetch_model_info(self, model_page: str):
|
||||||
if not model_page:
|
if not model_page:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
model_searcher = get_model_searcher_by_url(model_page)
|
model_searcher = self.get_model_searcher_by_url(model_page)
|
||||||
result = model_searcher.search_by_url(model_page)
|
result = model_searcher.search_by_url(model_page)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -435,3 +494,12 @@ class Information:
|
|||||||
utils.print_error(f"Failed to download model info for {abs_model_path}: {e}")
|
utils.print_error(f"Failed to download model info for {abs_model_path}: {e}")
|
||||||
|
|
||||||
utils.print_debug("Completed scan model information.")
|
utils.print_debug("Completed scan model information.")
|
||||||
|
|
||||||
|
def get_model_searcher_by_url(self, url: str) -> ModelSearcher:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
host_name = parsed_url.hostname
|
||||||
|
if host_name == "civitai.com":
|
||||||
|
return CivitaiModelSearcher()
|
||||||
|
elif host_name == "huggingface.co":
|
||||||
|
return HuggingfaceModelSearcher()
|
||||||
|
return UnknownWebsiteSearcher()
|
||||||
|
|||||||
@@ -124,17 +124,29 @@ class ModelManager:
|
|||||||
if not prefix_path.endswith("/"):
|
if not prefix_path.endswith("/"):
|
||||||
prefix_path = f"{prefix_path}/"
|
prefix_path = f"{prefix_path}/"
|
||||||
|
|
||||||
|
is_file = entry.is_file()
|
||||||
relative_path = utils.normalize_path(entry.path).replace(prefix_path, "")
|
relative_path = utils.normalize_path(entry.path).replace(prefix_path, "")
|
||||||
sub_folder = os.path.dirname(relative_path)
|
sub_folder = os.path.dirname(relative_path)
|
||||||
filename = os.path.basename(relative_path)
|
filename = os.path.basename(relative_path)
|
||||||
basename = os.path.splitext(filename)[0]
|
basename = os.path.splitext(filename)[0] if is_file else filename
|
||||||
extension = os.path.splitext(filename)[1]
|
extension = os.path.splitext(filename)[1] if is_file else ""
|
||||||
|
|
||||||
is_file = entry.is_file()
|
|
||||||
if is_file and extension not in folder_paths.supported_pt_extensions:
|
if is_file and extension not in folder_paths.supported_pt_extensions:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
model_preview = f"/model-manager/preview/{folder}/{path_index}/{relative_path.replace(extension, '.webp')}"
|
preview_type = "image"
|
||||||
|
preview_ext = ".webp"
|
||||||
|
preview_images = utils.get_model_all_images(entry.path)
|
||||||
|
if len(preview_images) > 0:
|
||||||
|
preview_type = "image"
|
||||||
|
preview_ext = ".webp"
|
||||||
|
else:
|
||||||
|
preview_videos = utils.get_model_all_videos(entry.path)
|
||||||
|
if len(preview_videos) > 0:
|
||||||
|
preview_type = "video"
|
||||||
|
preview_ext = f".{preview_videos[0].split('.')[-1]}"
|
||||||
|
|
||||||
|
model_preview = f"/model-manager/preview/{folder}/{path_index}/{relative_path.replace(extension, preview_ext)}"
|
||||||
|
|
||||||
stat = entry.stat()
|
stat = entry.stat()
|
||||||
return {
|
return {
|
||||||
@@ -146,6 +158,7 @@ class ModelManager:
|
|||||||
"pathIndex": path_index,
|
"pathIndex": path_index,
|
||||||
"sizeBytes": stat.st_size if is_file else 0,
|
"sizeBytes": stat.st_size if is_file else 0,
|
||||||
"preview": model_preview if is_file else None,
|
"preview": model_preview if is_file else None,
|
||||||
|
"previewType": preview_type,
|
||||||
"createdAt": round(stat.st_ctime_ns / 1000000),
|
"createdAt": round(stat.st_ctime_ns / 1000000),
|
||||||
"updatedAt": round(stat.st_mtime_ns / 1000000),
|
"updatedAt": round(stat.st_mtime_ns / 1000000),
|
||||||
}
|
}
|
||||||
|
|||||||
35
py/utils.py
35
py/utils.py
@@ -8,12 +8,13 @@ import requests
|
|||||||
import traceback
|
import traceback
|
||||||
import configparser
|
import configparser
|
||||||
import functools
|
import functools
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from . import config
|
from . import config
|
||||||
|
|
||||||
|
|
||||||
@@ -149,6 +150,20 @@ def resolve_model_base_paths() -> dict[str, list[str]]:
|
|||||||
return model_base_paths
|
return model_base_paths
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_file_content_type(filename: str):
|
||||||
|
extension_mimetypes_cache = folder_paths.extension_mimetypes_cache
|
||||||
|
extension = filename.split(".")[-1]
|
||||||
|
if extension not in extension_mimetypes_cache:
|
||||||
|
mime_type, _ = mimetypes.guess_type(filename, strict=False)
|
||||||
|
if not mime_type:
|
||||||
|
return None
|
||||||
|
content_type = mime_type.split("/")[0]
|
||||||
|
extension_mimetypes_cache[extension] = content_type
|
||||||
|
else:
|
||||||
|
content_type = extension_mimetypes_cache[extension]
|
||||||
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
def get_full_path(model_type: str, path_index: int, filename: str):
|
def get_full_path(model_type: str, path_index: int, filename: str):
|
||||||
"""
|
"""
|
||||||
Get the absolute path in the model type through string concatenation.
|
Get the absolute path in the model type through string concatenation.
|
||||||
@@ -266,6 +281,22 @@ def get_model_preview_name(model_path: str):
|
|||||||
return images[0] if len(images) > 0 else "no-preview.png"
|
return images[0] if len(images) > 0 else "no-preview.png"
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_all_videos(model_path: str):
|
||||||
|
base_dirname = os.path.dirname(model_path)
|
||||||
|
files = search_files(base_dirname)
|
||||||
|
files = folder_paths.filter_files_content_types(files, ["video"])
|
||||||
|
|
||||||
|
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||||
|
output: list[str] = []
|
||||||
|
for file in files:
|
||||||
|
file_basename = os.path.splitext(file)[0]
|
||||||
|
if file_basename == basename:
|
||||||
|
output.append(file)
|
||||||
|
if file_basename == f"{basename}.preview":
|
||||||
|
output.append(file)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
@@ -277,7 +308,7 @@ def remove_model_preview_image(model_path: str):
|
|||||||
os.remove(preview_path)
|
os.remove(preview_path)
|
||||||
|
|
||||||
|
|
||||||
def save_model_preview_image(model_path: str, image_file_or_url: Any, platform: str | None = None):
|
def save_model_preview_image(model_path: str, image_file_or_url: Any, platform: Optional[str] = None):
|
||||||
basename = os.path.splitext(model_path)[0]
|
basename = os.path.splitext(model_path)[0]
|
||||||
preview_path = f"{basename}.webp"
|
preview_path = f"{basename}.webp"
|
||||||
# Download image file if it is url
|
# Download image file if it is url
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-model-manager"
|
name = "comfyui-model-manager"
|
||||||
description = "Manage models: browsing, download and delete."
|
description = "Manage models: browsing, download and delete."
|
||||||
version = "2.5.1"
|
version = "2.5.4"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["markdownify"]
|
dependencies = ["markdownify"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<ResponseDialog
|
<ResponseDialog
|
||||||
v-for="(item, index) in stack"
|
v-for="(item, index) in stack"
|
||||||
v-model:visible="item.visible"
|
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:keep-alive="item.keepAlive"
|
v-model:visible="item.visible"
|
||||||
:default-size="item.defaultSize"
|
v-bind="omitProps(item)"
|
||||||
:default-mobile-size="item.defaultMobileSize"
|
|
||||||
:resize-allow="item.resizeAllow"
|
|
||||||
:min-width="item.minWidth"
|
|
||||||
:max-width="item.maxWidth"
|
|
||||||
:min-height="item.minHeight"
|
|
||||||
:max-height="item.maxHeight"
|
|
||||||
:auto-z-index="false"
|
:auto-z-index="false"
|
||||||
:pt:mask:style="{ zIndex: baseZIndex + index + 1 }"
|
:pt:mask:style="{ zIndex: baseZIndex + index + 1 }"
|
||||||
:pt:root:onMousedown="() => rise(item)"
|
:pt:root:onMousedown="() => rise(item)"
|
||||||
@@ -42,7 +35,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ResponseDialog from 'components/ResponseDialog.vue'
|
import ResponseDialog from 'components/ResponseDialog.vue'
|
||||||
import { useDialog } from 'hooks/dialog'
|
import { type DialogItem, useDialog } from 'hooks/dialog'
|
||||||
|
import { omit } from 'lodash'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { usePrimeVue } from 'primevue/config'
|
import { usePrimeVue } from 'primevue/config'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
@@ -55,4 +49,15 @@ const { config } = usePrimeVue()
|
|||||||
const baseZIndex = computed(() => {
|
const baseZIndex = computed(() => {
|
||||||
return config.zIndex?.modal ?? 1100
|
return config.zIndex?.modal ?? 1100
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const omitProps = (item: DialogItem) => {
|
||||||
|
return omit(item, [
|
||||||
|
'key',
|
||||||
|
'visible',
|
||||||
|
'title',
|
||||||
|
'headerButtons',
|
||||||
|
'content',
|
||||||
|
'contentProps',
|
||||||
|
])
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,6 +24,21 @@
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="model.previewType === 'video'"
|
||||||
|
class="h-full w-full p-1 hover:p-0"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
playsinline
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
disablepictureinpicture
|
||||||
|
preload="none"
|
||||||
|
>
|
||||||
|
<source :src="preview" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
<div v-else class="h-full w-full p-1 hover:p-0">
|
<div v-else class="h-full w-full p-1 hover:p-0">
|
||||||
<img class="h-full w-full rounded-lg object-cover" :src="preview" />
|
<img class="h-full w-full rounded-lg object-cover" :src="preview" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,24 @@
|
|||||||
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
|
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
|
||||||
:style="$sm({ width: `${cardWidth}px` })"
|
:style="$sm({ width: `${cardWidth}px` })"
|
||||||
>
|
>
|
||||||
<ResponseImage :src="preview" :error="noPreviewContent"></ResponseImage>
|
<div v-if="previewType === 'video'" class="h-full w-full p-1 hover:p-0">
|
||||||
|
<video
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
playsinline
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
disablepictureinpicture
|
||||||
|
preload="none"
|
||||||
|
>
|
||||||
|
<source :src="preview" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponseImage
|
||||||
|
v-else
|
||||||
|
:src="preview"
|
||||||
|
:error="noPreviewContent"
|
||||||
|
></ResponseImage>
|
||||||
|
|
||||||
<Carousel
|
<Carousel
|
||||||
v-if="defaultContent.length > 1"
|
v-if="defaultContent.length > 1"
|
||||||
@@ -95,6 +112,7 @@ const { cardWidth } = useConfig()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
preview,
|
preview,
|
||||||
|
previewType,
|
||||||
typeOptions,
|
typeOptions,
|
||||||
currentType,
|
currentType,
|
||||||
defaultContent,
|
defaultContent,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
:visible="true"
|
:visible="true"
|
||||||
@update:visible="updateVisible"
|
@update:visible="updateVisible"
|
||||||
|
:modal="modal"
|
||||||
:close-on-escape="false"
|
:close-on-escape="false"
|
||||||
:maximizable="!isMobile"
|
:maximizable="!isMobile"
|
||||||
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
||||||
@@ -91,6 +92,7 @@ interface Props {
|
|||||||
minHeight?: number
|
minHeight?: number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
zIndex?: number
|
zIndex?: number
|
||||||
|
modal?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface HeaderButton {
|
|||||||
command: () => void
|
command: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DialogItem {
|
export interface DialogItem {
|
||||||
key: string
|
key: string
|
||||||
title: string
|
title: string
|
||||||
content: Component
|
content: Component
|
||||||
@@ -22,6 +22,7 @@ interface DialogItem {
|
|||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
minHeight?: number
|
minHeight?: number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
|
modal?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialog = defineStore('dialog', () => {
|
export const useDialog = defineStore('dialog', () => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const useModelExplorer = () => {
|
|||||||
description: '',
|
description: '',
|
||||||
metadata: {},
|
metadata: {},
|
||||||
preview: '',
|
preview: '',
|
||||||
|
previewType: 'image',
|
||||||
type: folder ?? '',
|
type: folder ?? '',
|
||||||
isFolder: true,
|
isFolder: true,
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
@@ -579,6 +579,10 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
|||||||
return content
|
return content
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const previewType = computed(() => {
|
||||||
|
return model.value.previewType
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
registerReset(() => {
|
registerReset(() => {
|
||||||
currentType.value = 'default'
|
currentType.value = 'default'
|
||||||
@@ -594,6 +598,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
|||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
preview,
|
preview,
|
||||||
|
previewType,
|
||||||
typeOptions,
|
typeOptions,
|
||||||
currentType,
|
currentType,
|
||||||
// default value
|
// default value
|
||||||
|
|||||||
1
src/types/typings.d.ts
vendored
1
src/types/typings.d.ts
vendored
@@ -11,6 +11,7 @@ export interface BaseModel {
|
|||||||
pathIndex: number
|
pathIndex: number
|
||||||
isFolder: boolean
|
isFolder: boolean
|
||||||
preview: string | string[]
|
preview: string | string[]
|
||||||
|
previewType: string
|
||||||
description: string
|
description: string
|
||||||
metadata: Record<string, string>
|
metadata: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user