Feat resize model card (#104)
* feat: Use setting definition card size * refactor: Optimize computed value of the list items - Add useContainerResize hooks - Remove v-resize - Change cols to computed * refactor(ModelCard): Optimize style - Control the display of button or name in difference sizes - Add name tooltip when hiding name * feat: Add i18n * pref: optimize style code structure * feat: add quick resize card size * feat: add custom size tooltip * feat: optimize card tool button display judgment
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
:style="$content_lg(contentStyle)"
|
||||
>
|
||||
<div ref="toolbarContainer" class="col-span-full">
|
||||
<div class="flex flex-col gap-4" :style="$toolbar_2xl(toolbarStyle)">
|
||||
<div :class="['flex gap-4', $toolbar_2xl('flex-row', 'flex-col')]">
|
||||
<ResponseInput
|
||||
v-model="searchContent"
|
||||
:placeholder="$t('searchModels')"
|
||||
@@ -26,6 +26,10 @@
|
||||
v-model="sortOrder"
|
||||
:items="sortOrderOptions"
|
||||
></ResponseSelect>
|
||||
<ResponseSelect
|
||||
v-model="currentCardSize"
|
||||
:items="cardSizeOptions"
|
||||
></ResponseSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +76,7 @@ import { genModelKey } from 'utils/model'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { isMobile, cardWidth, gutter, aspect } = useConfig()
|
||||
const { isMobile, gutter, cardSize } = useConfig()
|
||||
|
||||
const { data, folders } = useModels()
|
||||
const { t } = useI18n()
|
||||
@@ -126,14 +130,14 @@ const sortOrderOptions = ref(
|
||||
)
|
||||
|
||||
const itemSize = computed(() => {
|
||||
let itemWidth = cardWidth
|
||||
let itemHeight = cardSize.value.height
|
||||
let itemGutter = gutter
|
||||
if (isMobile.value) {
|
||||
const baseSize = 16
|
||||
itemWidth = window.innerWidth - baseSize * 2 * 2
|
||||
itemHeight = window.innerWidth - baseSize * 2 * 2
|
||||
itemGutter = baseSize * 2
|
||||
}
|
||||
return itemWidth / aspect + itemGutter
|
||||
return itemHeight + itemGutter
|
||||
})
|
||||
|
||||
const { width } = useElementSize(contentContainer)
|
||||
@@ -142,9 +146,9 @@ const cols = computed(() => {
|
||||
if (isMobile.value) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const containerWidth = width.value
|
||||
return Math.floor((containerWidth - gutter) / (cardWidth + gutter))
|
||||
const itemWidth = cardSize.value.width
|
||||
return Math.floor((containerWidth - gutter) / (itemWidth + gutter))
|
||||
})
|
||||
|
||||
const list = computed(() => {
|
||||
@@ -187,13 +191,56 @@ const list = computed(() => {
|
||||
})
|
||||
|
||||
const contentStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(auto-fit, ${cardWidth}px)`,
|
||||
gridTemplateColumns: `repeat(auto-fit, ${cardSize.value.width}px)`,
|
||||
gap: `${gutter}px`,
|
||||
paddingLeft: `1rem`,
|
||||
paddingRight: `1rem`,
|
||||
}))
|
||||
|
||||
const toolbarStyle = computed(() => ({
|
||||
flexDirection: 'row',
|
||||
}))
|
||||
const currentCardSize = computed({
|
||||
get: () => {
|
||||
const options = cardSizeOptions.value.map((item) => item.value)
|
||||
const current = [cardSize.value.width, cardSize.value.height].join('x')
|
||||
if (options.includes(current)) {
|
||||
return current
|
||||
}
|
||||
return 'custom'
|
||||
},
|
||||
set: (val) => {
|
||||
if (val === 'custom') {
|
||||
app.ui?.settings.show(t('size.customTip'))
|
||||
return
|
||||
}
|
||||
|
||||
const [width, height] = val.split('x')
|
||||
app.ui?.settings.setSettingValue(
|
||||
'ModelManager.UI.CardWidth',
|
||||
parseInt(width),
|
||||
)
|
||||
app.ui?.settings.setSettingValue(
|
||||
'ModelManager.UI.CardHeight',
|
||||
parseInt(height),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const cardSizeOptions = computed(() => {
|
||||
const defineOptions = {
|
||||
extraLarge: '240x320',
|
||||
large: '180x240',
|
||||
medium: '120x160',
|
||||
small: '80x120',
|
||||
custom: 'custom',
|
||||
}
|
||||
|
||||
return Object.entries(defineOptions).map(([key, value]) => {
|
||||
return {
|
||||
label: t(`size.${key}`),
|
||||
value,
|
||||
command() {
|
||||
currentCardSize.value = value
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="group/card relative w-full cursor-pointer select-none preview-aspect"
|
||||
class="group/card relative cursor-pointer select-none"
|
||||
:style="{ width: `${cardSize.width}px`, height: `${cardSize.height}px` }"
|
||||
v-tooltip.top="{ value: model.basename, disabled: showModelName }"
|
||||
@click.stop="openDetailDialog"
|
||||
>
|
||||
<div class="h-full overflow-hidden rounded-lg">
|
||||
@@ -18,7 +20,7 @@
|
||||
|
||||
<div class="pointer-events-none absolute left-0 top-0 h-full w-full p-4">
|
||||
<div class="relative h-full w-full text-white">
|
||||
<div class="absolute bottom-0 left-0">
|
||||
<div v-show="showModelName" class="absolute bottom-0 left-0">
|
||||
<div class="drop-shadow-[0px_2px_2px_rgba(0,0,0,0.75)]">
|
||||
<div
|
||||
:class="[
|
||||
@@ -33,13 +35,19 @@
|
||||
|
||||
<div class="absolute left-0 top-0 w-full">
|
||||
<div class="flex flex-row items-start justify-between">
|
||||
<div class="flex items-center rounded-full bg-black/30 px-3 py-2">
|
||||
<div
|
||||
v-show="showModelType"
|
||||
class="flex items-center rounded-full bg-black/30 px-3 py-2"
|
||||
>
|
||||
<div :class="['font-bold', $lg('text-xs')]">
|
||||
{{ model.type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opacity-0 duration-300 group-hover/card:opacity-100">
|
||||
<div
|
||||
v-show="showToolButton"
|
||||
class="opacity-0 duration-300 group-hover/card:opacity-100"
|
||||
>
|
||||
<div class="flex flex-col gap-4 *:pointer-events-auto">
|
||||
<Button
|
||||
icon="pi pi-plus"
|
||||
@@ -71,6 +79,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
||||
import { useConfig } from 'hooks/config'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
import { useModelNodeAction } from 'hooks/model'
|
||||
@@ -85,6 +94,8 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { cardSize } = useConfig()
|
||||
|
||||
const dialog = useDialog()
|
||||
|
||||
const openDetailDialog = () => {
|
||||
@@ -105,6 +116,18 @@ const preview = computed(() =>
|
||||
: props.model.preview,
|
||||
)
|
||||
|
||||
const showToolButton = computed(() => {
|
||||
return cardSize.value.width >= 180 && cardSize.value.height >= 240
|
||||
})
|
||||
|
||||
const showModelName = computed(() => {
|
||||
return cardSize.value.width >= 160 && cardSize.value.height >= 120
|
||||
})
|
||||
|
||||
const showModelType = computed(() => {
|
||||
return cardSize.value.width >= 120
|
||||
})
|
||||
|
||||
const { addModelNode, dragToAddModelNode, copyModelNode, loadPreviewWorkflow } =
|
||||
useModelNodeAction(props.model)
|
||||
|
||||
|
||||
@@ -21,9 +21,19 @@ export const useConfig = defineStore('config', (store) => {
|
||||
window.removeEventListener('resize', checkDeviceType)
|
||||
})
|
||||
|
||||
const cardSize = ref({
|
||||
width:
|
||||
app.ui?.settings.getSettingValue<number>('ModelManager.UI.CardWidth') ??
|
||||
240,
|
||||
height:
|
||||
app.ui?.settings.getSettingValue<number>('ModelManager.UI.CardHeight') ??
|
||||
310,
|
||||
})
|
||||
|
||||
const config = {
|
||||
isMobile,
|
||||
gutter: 16,
|
||||
cardSize,
|
||||
cardWidth: 240,
|
||||
aspect: 7 / 9,
|
||||
}
|
||||
@@ -99,6 +109,39 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
defaultValue: undefined,
|
||||
})
|
||||
|
||||
// UI settings
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.UI.CardWidth',
|
||||
category: [t('modelManager'), t('setting.ui'), 'CardWidth'],
|
||||
name: t('setting.cardWidth'),
|
||||
type: 'slider',
|
||||
defaultValue: 240,
|
||||
attrs: {
|
||||
min: 80,
|
||||
max: 320,
|
||||
step: 10,
|
||||
},
|
||||
onChange(value) {
|
||||
store.config.cardSize.value.width = value
|
||||
},
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.UI.CardHeight',
|
||||
category: [t('modelManager'), t('setting.ui'), 'CardHeight'],
|
||||
name: t('setting.cardHeight'),
|
||||
type: 'slider',
|
||||
defaultValue: 320,
|
||||
attrs: {
|
||||
min: 80,
|
||||
max: 320,
|
||||
step: 10,
|
||||
},
|
||||
onChange(value) {
|
||||
store.config.cardSize.value.height = value
|
||||
},
|
||||
})
|
||||
|
||||
// Scan information
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.ScanFiles.Full',
|
||||
|
||||
22
src/i18n.ts
22
src/i18n.ts
@@ -31,6 +31,14 @@ const messages = {
|
||||
created: 'Latest created',
|
||||
modified: 'Latest modified',
|
||||
},
|
||||
size: {
|
||||
extraLarge: 'Extra Large Icons',
|
||||
large: 'Large Icons',
|
||||
medium: 'Medium Icons',
|
||||
small: 'Small Icons',
|
||||
custom: 'Custom Size',
|
||||
customTip: 'Set in `Settings > Model Manager > UI`',
|
||||
},
|
||||
info: {
|
||||
type: 'Model Type',
|
||||
pathIndex: 'Directory',
|
||||
@@ -41,11 +49,14 @@ const messages = {
|
||||
},
|
||||
setting: {
|
||||
apiKey: 'API Key',
|
||||
cardHeight: 'Card Height',
|
||||
cardWidth: 'Card Width',
|
||||
scan: 'Scan',
|
||||
scanMissing: 'Download missing information or preview',
|
||||
scanAll: "Override all models' information and preview",
|
||||
includeHiddenFiles: 'Include hidden files(start with .)',
|
||||
excludeScanTypes: 'Exclude scan types (separate with commas)',
|
||||
ui: 'UI',
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
@@ -77,6 +88,14 @@ const messages = {
|
||||
created: '最新创建',
|
||||
modified: '最新修改',
|
||||
},
|
||||
size: {
|
||||
extraLarge: '超大图标',
|
||||
large: '大图标',
|
||||
medium: '中等图标',
|
||||
small: '小图标',
|
||||
custom: '自定义尺寸',
|
||||
customTip: '在 `设置 > 模型管理器 > 外观` 中设置',
|
||||
},
|
||||
info: {
|
||||
type: '类型',
|
||||
pathIndex: '目录',
|
||||
@@ -87,11 +106,14 @@ const messages = {
|
||||
},
|
||||
setting: {
|
||||
apiKey: '密钥',
|
||||
cardHeight: '卡片高度',
|
||||
cardWidth: '卡片宽度',
|
||||
scan: '扫描',
|
||||
scanMissing: '下载缺失的信息或预览图片',
|
||||
scanAll: '覆盖所有模型信息和预览图片',
|
||||
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
||||
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
||||
ui: '外观',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -155,7 +155,7 @@ declare namespace ComfyAPI {
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
class ComfySettingsDialog {
|
||||
class ComfySettingsDialog extends dialog.ComfyDialog {
|
||||
addSetting: (params: SettingParams) => { value: any }
|
||||
getSettingValue: <T>(id: string, defaultValue?: T) => T
|
||||
setSettingValue: <T>(id: string, value: T) => void
|
||||
|
||||
Reference in New Issue
Block a user