first working version / change indicators, bulk api missing

pull/526/head
Florian Geiselhart 2024-02-29 15:15:27 +01:00
parent 0d09f47c95
commit 1db5689734
6 changed files with 439 additions and 6 deletions

5
vue/components.d.ts vendored
View File

@ -14,12 +14,15 @@ declare module '@vue/runtime-core' {
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
@ -36,8 +39,10 @@ declare module '@vue/runtime-core' {
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BaseFileListInfo: typeof import('./src/components/BaseFileListInfo.vue')['default']
ChangeIndicator: typeof import('./src/components/ChangeIndicator.vue')['default']
ContextMenu: typeof import('./src/components/ContextMenu.vue')['default']
FileItem: typeof import('./src/components/FileItem.vue')['default']
NumInput: typeof import('./src/components/numInput.vue')['default']

View File

@ -9,6 +9,15 @@ export interface FileNodeInfo {
bytes: number
fullpath: string
is_under_scanned_path: boolean
gen_info_raw?: string
gen_info_obj?: object
}
export interface GenDiffInfo {
empty: boolean
ownFile: string
otherFile: string
diff: object
}
export const getTargetFolderFiles = async (folder_path: string) => {

View File

@ -0,0 +1,301 @@
<script setup lang="ts">
import { CaretRightOutlined } from '@/icon'
import type { GenDiffInfo } from '@/api/files'
const props = defineProps<{
genDiffToPrevious: GenDiffInfo
genDiffToNext: GenDiffInfo
genInfo?: string
}>()
function filterManualProps(diff: Record<string, unknown>) {
const manualProps = ['prompt', 'negativePrompt', 'seed', 'steps', 'cfgScale', 'size', 'Model', 'others']
const otherKeys = Object.keys(diff).filter((key) => !manualProps.includes(key))
return Object.fromEntries(otherKeys.map((key) => [key, diff[key]]))
}
function hasOtherProps(diff: Record<string, unknown>) {
return Object.keys(filterManualProps(diff)).length > 0
}
</script>
<template>
<div class="changeIndicatorWrapper">
<div class="changeIndicatorsLeft changeIndicators" v-if="!genDiffToPrevious.empty">
<div class="promptChangeIndicator changeIndicator" v-if="'prompt' in genDiffToPrevious.diff">P+</div>
<div class="negpromptChangeIndicator changeIndicator" v-if="'negativePrompt' in genDiffToPrevious.diff">P-</div>
<div class="seedChangeIndicator changeIndicator" v-if="'seed' in genDiffToPrevious.diff">Se</div>
<div class="stepsChangeIndicator changeIndicator" v-if="'steps' in genDiffToPrevious.diff">St</div>
<div class="cfgChangeIndicator changeIndicator" v-if="'cfgScale' in genDiffToPrevious.diff!">Cf</div>
<div class="sizeChangeIndicator changeIndicator" v-if="'size' in genDiffToPrevious.diff">Si</div>
<div class="modelChangeIndicator changeIndicator" v-if="'Model' in genDiffToPrevious.diff">Mo</div>
<div class="otherChangeIndicator changeIndicator" v-if="hasOtherProps(genDiffToPrevious.diff)">Ot</div>
</div>
<div class="hoverOverlay">
<small>
<CaretRightOutlined /><strong>This file</strong> vs {{ genDiffToPrevious.otherFile }}
<br /><br />
<!-- fixed props -->
<table>
<tr v-if="'prompt' in genDiffToPrevious.diff">
<td><span class="promptChangeIndicator">+ Prompt</span></td>
<td>{{ genDiffToPrevious.diff.prompt }} tokens changed</td>
</tr>
<tr v-if="'negativePrompt' in genDiffToPrevious.diff">
<td><span class="negpromptChangeIndicator">- Prompt</span></td>
<td>{{ genDiffToPrevious.diff.negativePrompt }} tokens changed</td>
</tr>
<tr v-if="'seed' in genDiffToPrevious.diff">
<td><span class="seedChangeIndicator">Seed</span></td>
<td><strong>{{ genDiffToPrevious.diff.seed[0] }}</strong> vs {{ genDiffToPrevious.diff.seed[1] }}</td>
</tr>
<tr v-if="'steps' in genDiffToPrevious.diff">
<td><span class="stepsChangeIndicator">Steps</span></td>
<td><strong>{{ genDiffToPrevious.diff.steps[0] }}</strong> vs {{ genDiffToPrevious.diff.steps[1] }}
</td>
</tr>
<tr v-if="'cfgScale' in genDiffToPrevious.diff">
<td><span class="cfgChangeIndicator">Cfg Scale</span></td>
<td><strong>{{ genDiffToPrevious.diff.cfgScale[0] }}</strong> vs {{
genDiffToPrevious.diff.cfgScale[1] }}</td>
</tr>
<tr v-if="'size' in genDiffToPrevious.diff">
<td><span class="sizeChangeIndicator">Size</span></td>
<td><strong>{{ genDiffToPrevious.diff.size[0] }}</strong> vs {{ genDiffToPrevious.diff.size[1] }}
</td>
</tr>
<tr v-if="'Model' in genDiffToPrevious.diff">
<td><span class="modelChangeIndicator">Model</span></td>
<td><strong>{{ genDiffToPrevious.diff.Model[0] }}</strong><br/> vs {{ genDiffToPrevious.diff.Model[1] }}
</td>
</tr>
</table>
<br />
<!-- others -->
<div v-if="hasOtherProps(genDiffToPrevious.diff)">
<span class="otherChangeIndicator">Other</span> props that changed:
<ul>
<li v-for="(value, propertyName) in filterManualProps(genDiffToPrevious.diff)">{{ propertyName }}
</li>
</ul>
</div>
</small>
</div>
<div class="changeIndicatorsRight changeIndicators" v-if="!genDiffToNext.empty">
<div class="promptChangeIndicator changeIndicator" v-if="'prompt' in genDiffToNext.diff">P+</div>
<div class="negpromptChangeIndicator changeIndicator" v-if="'negativePrompt' in genDiffToNext.diff">P-</div>
<div class="seedChangeIndicator changeIndicator" v-if="'seed' in genDiffToNext.diff">Se</div>
<div class="stepsChangeIndicator changeIndicator" v-if="'steps' in genDiffToNext.diff">St</div>
<div class="cfgChangeIndicator changeIndicator" v-if="'cfgScale' in genDiffToNext.diff">Cf</div>
<div class="sizeChangeIndicator changeIndicator" v-if="'size' in genDiffToNext.diff">Si</div>
<div class="modelChangeIndicator changeIndicator" v-if="'Model' in genDiffToNext.diff">Mo</div>
<div class="otherChangeIndicator changeIndicator" v-if="hasOtherProps(genDiffToNext.diff)">Ot</div>
</div>
<div class="hoverOverlay">
<small>
<CaretRightOutlined /><strong>This file</strong> vs {{ genDiffToNext.otherFile }}
<br /><br />
<!-- fixed props -->
<table>
<tr v-if="'prompt' in genDiffToNext.diff">
<td><span class="promptChangeIndicator">+ Prompt</span></td>
<td>{{ genDiffToNext.diff.prompt }} tokens changed</td>
</tr>
<tr v-if="'negativePrompt' in genDiffToNext.diff">
<td><span class="negpromptChangeIndicator">- Prompt</span></td>
<td>{{ genDiffToNext.diff.negativePrompt }} tokens changed</td>
</tr>
<tr v-if="'seed' in genDiffToNext.diff">
<td><span class="seedChangeIndicator">Seed</span></td>
<td><strong>{{ genDiffToNext.diff.seed[0] }}</strong> vs {{ genDiffToNext.diff.seed[1] }}</td>
</tr>
<tr v-if="'steps' in genDiffToNext.diff">
<td><span class="stepsChangeIndicator">Steps</span></td>
<td><strong>{{ genDiffToNext.diff.steps[0] }}</strong> vs {{ genDiffToNext.diff.steps[1] }}</td>
</tr>
<tr v-if="'cfgScale' in genDiffToNext.diff">
<td><span class="cfgChangeIndicator">Cfg Scale</span></td>
<td><strong>{{ genDiffToNext.diff.cfgScale[0] }}</strong> vs {{ genDiffToNext.diff.cfgScale[1] }}
</td>
</tr>
<tr v-if="'size' in genDiffToNext.diff">
<td><span class="sizeChangeIndicator">Size</span></td>
<td><strong>{{ genDiffToNext.diff.size[0] }}</strong> vs {{ genDiffToNext.diff.size[1] }}</td>
</tr>
<tr v-if="'Model' in genDiffToNext.diff">
<td><span class="modelChangeIndicator">Model</span></td>
<td><strong>{{ genDiffToNext.diff.Model[0] }}</strong><br/> vs {{ genDiffToNext.diff.Model[1] }}</td>
</tr>
</table>
<br />
<!-- others -->
<div v-if="hasOtherProps(genDiffToNext.diff)">
<span class="otherChangeIndicator">Other</span> props that changed:
<ul>
<li v-for="(value, propertyName) in filterManualProps(genDiffToNext.diff)">{{ propertyName }}
</li>
</ul>
</div>
</small>
</div>
</div>
</template>
<style lang="scss" scoped>
.changeIndicators {
position: absolute;
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
justify-content: center;
opacity: 0.6;
}
.changeIndicatorsRight {
position: absolute;
right: 0;
}
.changeIndicator {
margin-left: -4px;
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: gray;
line-height: 16px;
margin-bottom: 2px;
text-align: center;
font-size: 6pt;
font-weight: 600;
color: black;
z-index: 9999;
pointer-events: auto;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
}
.changeIndicatorsRight .changeIndicator {
margin-right: -4px;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
text-align: left;
padding-left: 2px;
}
.changeIndicatorsLeft .changeIndicator {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
text-align: right;
padding-right: 2px;
}
.changeIndicatorWrapper {
top: 0;
position: absolute;
user-select: none;
width: 100%;
height: 100%;
z-index: 999999;
pointer-events: none;
}
.hoverOverlay {
display: none;
background-color: rgba(0, 0, 0, 0.8);
color: white;
border: 1px solid gray;
padding: 10px;
padding-left: 20px;
padding-right: 20px;
border-radius: 5px;
z-index: 100;
opacity: 1;
font-size: 8pt;
line-height: 1.2;
overflow: hidden;
}
.hoverOverlay ul {
list-style: none;
padding: 0;
}
.hoverOverlay ul li {
display: inline-block;
padding-left: 4px;
padding-right: 4px;
border: 1px solid gray;
border-radius: 2px;
margin: 1px;
font-weight: 200;
}
.changeIndicators:hover {
opacity: 1;
}
.changeIndicators:hover+div.hoverOverlay {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
table tr td:first-child span {
padding: 1px;
padding-left:3px;
padding-right:3px;
display: inline-block;
width: 100%;
}
table tr td:first-child {
padding-right:10px;
vertical-align: top;
}
.otherChangeIndicator {
background-color: #551f58;
color: #efefef;
}
.stepsChangeIndicator {
background-color: #1c56bb;
color: #efefef;
}
.seedChangeIndicator {
background-color: #0593A2;
color: #112211;
}
.negpromptChangeIndicator {
background-color: #FF7A48;
color: #112211;
}
.modelChangeIndicator {
background-color: #E3371E;
color: #efefef;
}
.promptChangeIndicator {
background-color: #89D99D;
color: #112211;
}
.cfgChangeIndicator {
background-color: #FFD700;
color: #112211;
}
.sizeChangeIndicator {
background-color: #254b2a;
color: #efefef;
}</style>

View File

@ -8,10 +8,12 @@ import { toImageThumbnailUrl, toRawFileUrl } from '@/util/file'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { computed } from 'vue'
import ContextMenu from './ContextMenu.vue'
import ChangeIndicator from './ChangeIndicator.vue'
import { useTagStore } from '@/store/useTagStore'
import { CloseCircleOutlined, StarFilled, StarOutlined, PlayCircleFilled } from '@/icon'
import { Tag } from '@/api/db'
import { openVideoModal } from './functionalCallableComp'
import type { GenDiffInfo } from '@/api/files'
const global = useGlobalStore()
const tagStore = useTagStore()
@ -26,6 +28,10 @@ const props = withDefaults(
enableRightClickMenu?: boolean,
enableCloseIcon?: boolean
isSelectedMutilFiles?: boolean
genDiffToPrevious: GenDiffInfo
genDiffToNext: GenDiffInfo
genInfo?: string
enableChangeIndicator: boolean
}>(),
{ selected: false, enableRightClickMenu: true, enableCloseIcon: false }
)
@ -105,6 +111,10 @@ const taggleLikeTag = () => {
这么复杂是因为再全屏查看时可能因为直接删除导致fullpath变化然后整个预览直接退出-->
<div :key="file.fullpath" :class="`idx-${idx} item-content`" v-if="isImageFile(file.name)">
<!-- change indicators -->
<ChangeIndicator v-if="enableChangeIndicator" :gen-diff-to-next="genDiffToNext" :gen-diff-to-previous="genDiffToPrevious"/>
<!-- change indicators END -->
<a-image :src="imageSrc" :fallback="fallbackImage" :preview="{
src: fullScreenPreviewImageUrl,
onVisibleChange: (v: boolean, lv: boolean) => emit('previewVisibleChange', v, lv)
@ -152,6 +162,7 @@ const taggleLikeTag = () => {
</a-dropdown>
</template>
<style lang="scss" scoped>
.center {
display: flex;
justify-content: center;
@ -322,5 +333,4 @@ const taggleLikeTag = () => {
flex-direction: column;
align-items: flex-end;
}
}
</style>
}</style>

View File

@ -38,6 +38,8 @@ export const en: IIBI18nMap = {
other: 'Other',
livePreview: 'Live Preview',
gridCellWidth: 'Grid Cell Width (px)',
showChangeIndicators: 'Show Change Indicators',
seedAsChange: 'Seed as Change',
defaultGridCellWidth: 'Default Grid Cell Width (px)',
thumbnailResolution: 'Thumbnail Resolution (px)',
inputTargetFolderPath: 'Enter the absolute path of the target folder',

View File

@ -23,9 +23,12 @@ import FileItem from '@/components/FileItem.vue'
import fullScreenContextMenu from './fullScreenContextMenu.vue'
import BaseFileListInfo from '@/components/BaseFileListInfo.vue'
import { copy2clipboardI18n } from '@/util'
import { openFolder } from '@/api'
import { openFolder, getImageGenerationInfo } from '@/api'
import { sortMethods } from './fileSort'
import { isTauri } from '@/util/env'
import { parse } from 'stable-diffusion-image-metadata'
import { ref } from 'vue'
import type { GenDiffInfo } from '@/api/files'
const global = useGlobalStore()
const props = defineProps<{
@ -83,7 +86,99 @@ watch(
{ immediate: true }
)
const changeIndchecked = ref<boolean>(true);
const seedChangeChecked = ref<boolean>(false);
function getRawGenParams() {
for (let f in sortedFiles.value) {
if (sortedFiles.value[f].gen_info_raw) {
continue
}
let path = sortedFiles.value[f].fullpath
q.pushAction(() => getImageGenerationInfo(path)).res.then((v) => {
sortedFiles.value[f].gen_info_raw = v
sortedFiles.value[f].gen_info_obj = parse(v)
})
}
}
function getGenDiff(ownGenInfo: any, idx: any, increment: any, ownFileName?: any) {
//init result obj
let result: GenDiffInfo = {
diff: {},
empty: true,
ownFile: "",
otherFile: ""
}
//check for out of bounds
if(idx + increment < 0
|| idx + increment >= sortedFiles.value.length
|| sortedFiles.value[idx] == undefined) {
return result
}
//check for gen_info_obj existence
if(!("gen_info_obj" in sortedFiles.value[idx])
|| !("gen_info_obj" in sortedFiles.value[idx + increment])) {
return result
}
//diff vars init
let gen_a = ownGenInfo
let gen_b = sortedFiles.value[idx + increment].gen_info_obj
if(gen_b == undefined) {
return result
}
//further vars
let skip = ["hashes", "resources"]
result.diff = {}
result.ownFile = ownFileName,
result.otherFile = sortedFiles.value[idx + increment].name,
result.empty = false
if(!seedChangeChecked.value) {
skip.push("seed")
}
//actual per property diff
for (let k in gen_a) {
//skip unwanted values
if (skip.includes(k)) {
continue
}
//for all non-identical values, compare type based
//existence test
if (!(k in gen_b)) {
result.diff[k] = "+"
continue
}
//content test
if (gen_a[k] != gen_b[k]) {
if(k.includes("rompt") && gen_a[k] != "" && gen_b[k] != "") {
//prompt values are comma separated, handle them differently
let tokenize_a = gen_a[k].split(",")
let tokenize_b = gen_b[k].split(",")
//count how many tokens are different or at a different place
let diff_count = 0
for (let i in tokenize_a) {
if(tokenize_a[i] != tokenize_b[i]) {
diff_count++
}
}
result.diff[k] = diff_count;
} else {
//all others
result.diff[k] = [gen_a[k],gen_b[k]]
}
}
}
//result
return result
}
getRawGenParams();
</script>
<template>
@ -191,6 +286,12 @@ watch(
<a-form-item :label="$t('sortingMethod')">
<search-select v-model:value="sortMethod" @click.stop :conv="sortMethodConv" :options="sortMethods" />
</a-form-item>
<a-form-item :label="$t('showChangeIndicators')">
<a-switch v-model:checked="changeIndchecked"/>
</a-form-item>
<a-form-item :label="$t('seedAsChange')">
<a-switch v-model:checked="seedChangeChecked" :disabled="!changeIndchecked"/>
</a-form-item>
<div style="padding: 4px;">
<a @click.prevent="addToSearchScanPathAndQuickMove" v-if="!searchPathInfo">{{
$t('addToSearchScanPathAndQuickMove') }}</a>
@ -211,7 +312,8 @@ watch(
</div>
<div v-if="currPage" class="view">
<RecycleScroller class="file-list" :items="sortedFiles" ref="scroller" @scroll="onScroll"
:item-size="itemSize.first" key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems">
v-on:scroll="getRawGenParams()" :item-size="itemSize.first" key-field="fullpath"
:item-secondary-size="itemSize.second" :gridItems="gridItems">
<template v-slot="{ item: file, index: idx }">
<!-- idx 和file有可能丢失 -->
<file-item :idx="parseInt(idx)" :file="file"
@ -219,7 +321,11 @@ watch(
v-model:show-menu-idx="showMenuIdx" :selected="multiSelectedIdxs.includes(idx)" :cell-width="cellWidth"
@file-item-click="onFileItemClick" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
@preview-visible-change="onPreviewVisibleChange" @context-menu-click="onContextMenuClick"
:is-selected-mutil-files="multiSelectedIdxs.length > 1" />
:is-selected-mutil-files="multiSelectedIdxs.length > 1"
:gen-diff-to-next="getGenDiff(file.gen_info_obj, idx, 1, file.name)"
:gen-diff-to-previous="getGenDiff(file.gen_info_obj, idx, -1, file.name)"
:enable-change-indicator="changeIndchecked"
/>
</template>
<template #after>
<div style="padding: 16px 0 64px;">
@ -228,7 +334,7 @@ watch(
{{ $t('loadNextPage') }}</AButton>
</div>
</template>
</RecycleScroller>
<div v-if="previewing" class="preview-switch">
<LeftCircleOutlined @click="previewImgMove('prev')" :class="{ disable: !canPreview('prev') }" />