dikdimon's picture
Upload extensions using SD-Hub extension
3dabe4a verified
import type { GlobalConf } from '@/api'
import type { ExtraPathType, MatchImageByTagsReq, Tag } from '@/api/db'
import { FileNodeInfo } from '@/api/files'
import { i18n, t } from '@/i18n'
import { getPreferredLang } from '@/i18n'
import { SortMethod } from '@/page/fileTransfer/fileSort'
import { Props as FileTransferProps } from '@/page/fileTransfer/hooks'
import type { getQuickMovePaths } from '@/page/taskRecord/autoComplete'
import { type Dict, type ReturnTypeAsync } from '@/util'
import { AnyFn, usePreferredDark } from '@vueuse/core'
import { cloneDeep, uniqueId, last } from 'lodash-es'
import { defineStore } from 'pinia'
import { VNode, computed, onMounted, reactive, toRaw, watch } from 'vue'
import { ref } from 'vue'
import { WithRequired } from 'vue3-ts-util'
import * as Path from '../util/path'
import { prefix } from '@/util/const'
interface TabPaneBase {
name: string | VNode
nameFallbackStr?: string
readonly key: string
}
interface OtherTabPane extends TabPaneBase {
type: 'global-setting' | 'tag-search' | 'batch-download' | 'workspace-snapshot'
}
export interface EmptyStartTabPane extends TabPaneBase {
type: 'empty'
popAddPathModal?: {
path: string
type: ExtraPathType
}
}
export type GridViewFileTag = WithRequired<Partial<Tag>, 'name'>;
export interface GridViewFile extends FileNodeInfo {
/**
* Tags for displaying the file. The 'name' property is required,
* while the other properties are optional.
*/
tags?: GridViewFileTag[];
}
/**
* A tab pane that displays files in a grid view.
*/
interface GridViewTabPane extends TabPaneBase {
type: 'grid-view'
/**
* Indicates whether the files in the grid view can be deleted.
*/
removable?: boolean
/**
* Indicates whether files can be dragged and dropped from other pages into the grid view.
*/
allowDragAndDrop?: boolean,
files: GridViewFile[]
}
export interface GridViewFile extends FileNodeInfo {
/**
* Tags for displaying the file. The 'name' property is required,
* while the other properties are optional.
*/
tags?: GridViewFileTag[];
}
/**
* A tab pane that displays files in a grid view.
*/
interface GridViewTabPane extends TabPaneBase {
type: 'grid-view'
/**
* Indicates whether the files in the grid view can be deleted.
*/
removable?: boolean
/**
* Indicates whether files can be dragged and dropped from other pages into the grid view.
*/
allowDragAndDrop?: boolean,
files: GridViewFile[]
}
interface TagSearchMatchedImageGridTabPane extends TabPaneBase {
type: 'tag-search-matched-image-grid'
selectedTagIds: MatchImageByTagsReq
id: string
}
export interface ImgSliTabPane extends TabPaneBase {
type: 'img-sli'
left: FileNodeInfo
right: FileNodeInfo
}
export interface FileTransferTabPane extends TabPaneBase {
type: 'local'
path?: string
mode?: FileTransferProps['mode']
stackKey?: string
}
export interface TagSearchTabPane extends TabPaneBase {
type: 'tag-search'
searchScope?: string
}
export interface FuzzySearchTabPane extends TabPaneBase {
type: 'fuzzy-search'
searchScope?: string
}
export type TabPane = EmptyStartTabPane | FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane| GridViewTabPane
/**
* This interface represents a tab, which contains an array of panes, an ID, and a key
*/
export interface Tab {
/**
* An array of panes that belong to this tab
*/
panes: TabPane[]
/**
* A unique identifier for this tab
*/
id: string
/**
* A value indicating which pane is currently selected within the tab
*/
key: string
}
export type Shortcut = Record<`toggle_tag_${string}` | 'delete' | 'download', string | undefined>
export type DefaultInitinalPage = `workspace_snapshot_${string}` | 'empty' | 'last-workspace-state'
export const copyPane = (pane: TabPane) => {
return cloneDeep({
...pane,
name: typeof pane.name === 'string' ? pane.name : pane.nameFallbackStr ?? ''
})
}
export const copyTab = (tab: Tab): Tab => {
return {
...tab,
panes: tab.panes.map(copyPane)
}
}
export const copyTabFilterWorkspaceSnapShot = (tab: Tab): Tab => {
if (!tab.panes.some(v => v.type === 'workspace-snapshot')) {
return copyTab(tab)
}
const newPanes = tab.panes.filter(v => v.type !== 'workspace-snapshot').map(copyPane)
return {
...tab,
panes: newPanes,
key: last(newPanes)?.key ?? ''
}
}
export type ActionConfirmRequired = 'deleteOneOnly'
export const presistKeys = [
'defaultChangeIndchecked',
'defaultSeedChangeChecked',
'darkModeControl',
'dontShowAgainNewImgOpts',
'defaultSortingMethod',
'defaultGridCellWidth',
'dontShowAgain',
'lang',
'enableThumbnail',
'tabListHistoryRecord',
'recent',
'gridThumbnailResolution',
'longPressOpenContextMenu',
'onlyFoldersAndImages',
'shortcut',
'ignoredConfirmActions',
'previewBgOpacity',
'defaultInitinalPage',
'autoRefreshWalkMode',
'autoRefreshWalkModePosLimit',
'autoRefreshNormalFixedMode',
'showCommaInInfoPanel'
]
function cellWidthMap(x: number): number {
if (x < 768) {
return 176;
} else {
const y = 160 + Math.floor((x - 768) / 128) * 16;
return Math.min(y, 256);
}
}
export const useGlobalStore = defineStore(
prefix + 'useGlobalStore',
() => {
const conf = ref<GlobalConf>()
const quickMovePaths = ref([] as ReturnTypeAsync<typeof getQuickMovePaths>)
const enableThumbnail = ref(true)
const gridThumbnailResolution = ref(512)
const defaultSortingMethod = ref(SortMethod.CREATED_TIME_DESC)
const defaultGridCellWidth = ref(cellWidthMap(parent.window.innerHeight))
const darkModeControl = ref<'light' | 'dark' | 'auto'>('auto')
const createEmptyPane = (): TabPane => ({
type: 'empty',
name: t('emptyStartPage'),
key: uniqueId()
})
const tabList = ref<Tab[]>([])
onMounted(() => {
const emptyPane = createEmptyPane()
tabList.value.push({ panes: [emptyPane], key: emptyPane.key, id: uniqueId() })
})
const dragingTab = ref<{ tabIdx: number; paneIdx: number }>()
const recent = ref(new Array<{ path: string; key: string, mode: FileTransferTabPane['mode'] }>())
const time = Date.now()
const tabListHistoryRecord = ref<{ time: number; tabs: Tab[] }[]>() // [curr,last]
const saveRecord = () => {
const tabs = toRaw(tabList.value).map(copyTab)
if (tabListHistoryRecord.value?.[0].time !== time) {
tabListHistoryRecord.value = [{ tabs, time }, ...(tabListHistoryRecord.value ?? [])]
} else {
tabListHistoryRecord.value[0].tabs = tabs
}
tabListHistoryRecord.value = tabListHistoryRecord.value.slice(0, 2)
}
const openTagSearchMatchedImageGridInRight = async (
tabIdx: number,
id: string,
tagIds: MatchImageByTagsReq
) => {
let pane = tabList.value
.map((v) => v.panes)
.flat()
.find(
(v) => v.type === 'tag-search-matched-image-grid' && v.id === id
) as TagSearchMatchedImageGridTabPane
if (pane) {
pane.selectedTagIds = cloneDeep(tagIds)
return
} else {
pane = {
type: 'tag-search-matched-image-grid',
id: id,
selectedTagIds: cloneDeep(tagIds),
key: uniqueId(),
name: t('searchResults')
}
}
const tab = tabList.value[tabIdx + 1]
if (!tab) {
tabList.value.push({ panes: [pane], key: pane.key, id: uniqueId() })
} else {
tab.key = pane.key
tab.panes.push(pane)
}
}
const lang = ref(getPreferredLang())
watch(lang, (v) => (i18n.global.locale.value = v as any))
const longPressOpenContextMenu = ref(false)
const shortcut = ref<Shortcut>({
delete: '',
download: ''
})
const extraPathAliasMap = ref({} as Dict<string>)
const pathAliasMap = computed((): Dict<string> => {
const keys = [
'outdir_extras_samples',
'outdir_save',
'outdir_txt2img_samples',
'outdir_img2img_samples',
'outdir_img2img_grids',
'outdir_txt2img_grids'
]
const res = quickMovePaths.value.filter((v) => keys.includes(v.key)).map((v) => [v.zh, v.dir])
return {...Object.fromEntries(res), ...extraPathAliasMap.value}
})
const pageFuncExportMap = new Map<string, Dict<AnyFn>>()
const ignoredConfirmActions = reactive<Record<ActionConfirmRequired, boolean>>({ deleteOneOnly: false })
const dark = usePreferredDark()
const computedTheme = computed(() => {
const getParDark = () => {
try {
return parent.location.search.includes('theme=dark') // sd-webui的
} catch (error) {
return false
}
}
const isDark = darkModeControl.value === 'auto' ? (dark.value || getParDark()) : (darkModeControl.value === 'dark')
return isDark ? 'dark' : 'light'
})
// 简化路径
const getShortPath = (loc: string) => {
try {
loc = loc.trim()
const map = pathAliasMap.value
const np = Path.normalize(loc)
const replacedPaths = [] as string[]
for (const [k, v] of Object.entries(map)) {
if (k && v) {
if (loc === v || np === v) return k
replacedPaths.push(np.replace(v, '$' + k))
}
}
return replacedPaths.sort((a, b) => a.length - b.length)?.[0] ?? loc
} catch (error) {
console.error(error)
return loc
}
}
const previewBgOpacity = ref(0.6)
return {
computedTheme,
darkModeControl,
defaultSortingMethod,
defaultGridCellWidth,
defaultChangeIndchecked: ref(true),
defaultSeedChangeChecked: ref(false),
pathAliasMap,
createEmptyPane,
lang,
tabList,
conf,
quickMovePaths,
enableThumbnail,
dragingTab,
saveRecord,
recent,
tabListHistoryRecord,
gridThumbnailResolution,
longPressOpenContextMenu,
openTagSearchMatchedImageGridInRight,
onlyFoldersAndImages: ref(true),
keepMultiSelect: ref(false),
fullscreenPreviewInitialUrl: ref(''),
shortcut,
pageFuncExportMap,
dontShowAgain: ref(false),
dontShowAgainNewImgOpts: ref(false),
ignoredConfirmActions,
getShortPath,
extraPathAliasMap,
previewBgOpacity,
defaultInitinalPage: ref<DefaultInitinalPage>('empty'),
autoRefreshWalkMode: ref(true),
autoRefreshWalkModePosLimit: ref(128),
autoRefreshNormalFixedMode: ref(true),
showCommaInInfoPanel: ref(false),
}
},
{
persist: {
// debug: true,
paths: presistKeys
}
}
)