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, '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() const quickMovePaths = ref([] as ReturnTypeAsync) 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([]) 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({ delete: '', download: '' }) const extraPathAliasMap = ref({} as Dict) const pathAliasMap = computed((): Dict => { 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>() const ignoredConfirmActions = reactive>({ 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('empty'), autoRefreshWalkMode: ref(true), autoRefreshWalkModePosLimit: ref(128), autoRefreshNormalFixedMode: ref(true), showCommaInInfoPanel: ref(false), } }, { persist: { // debug: true, paths: presistKeys } } )