271 lines
7.0 KiB
TypeScript
271 lines
7.0 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import type { FontInfo, FontTreeNode } from '../types/font'
|
|
import { loadFontWithProgress } from '../utils/font-loader'
|
|
|
|
export const useFontStore = defineStore('font', () => {
|
|
function readSet(key: string): Set<string> {
|
|
try {
|
|
const raw = localStorage.getItem(key)
|
|
if (!raw) return new Set()
|
|
const parsed = JSON.parse(raw)
|
|
return Array.isArray(parsed) ? new Set(parsed.map(String)) : new Set()
|
|
} catch {
|
|
return new Set()
|
|
}
|
|
}
|
|
|
|
function writeSet(key: string, value: Set<string>) {
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify(Array.from(value)))
|
|
} catch {
|
|
// ignore storage errors
|
|
}
|
|
}
|
|
|
|
function filterSetByValidIds(source: Set<string>, validIds: Set<string>): Set<string> {
|
|
return new Set(Array.from(source).filter(id => validIds.has(id)))
|
|
}
|
|
|
|
// 状态
|
|
const fonts = ref<FontInfo[]>([])
|
|
const selectedFontIds = ref<Set<string>>(new Set())
|
|
const favoriteFontIds = ref<Set<string>>(readSet('font.favoriteFontIds'))
|
|
const previewFontIds = ref<Set<string>>(readSet('font.previewFontIds'))
|
|
const expandedCategoryNames = ref<Set<string>>(readSet('font.expandedCategories'))
|
|
const fontTree = ref<FontTreeNode[]>([])
|
|
const isLoadingFonts = ref(false)
|
|
|
|
// 计算属性
|
|
const selectedFonts = computed(() => {
|
|
return fonts.value.filter(f => selectedFontIds.value.has(f.id))
|
|
})
|
|
|
|
const favoriteFonts = computed(() => {
|
|
return fonts.value.filter(f => favoriteFontIds.value.has(f.id))
|
|
})
|
|
|
|
const previewFonts = computed(() => {
|
|
return fonts.value.filter(f => previewFontIds.value.has(f.id))
|
|
})
|
|
|
|
const favoriteTree = computed(() => {
|
|
return buildFontTree(favoriteFonts.value)
|
|
})
|
|
|
|
// 方法
|
|
function addFont(fontInfo: FontInfo) {
|
|
fonts.value.push(fontInfo)
|
|
}
|
|
|
|
function replaceFonts(nextFonts: FontInfo[]) {
|
|
const validIds = new Set(nextFonts.map(font => font.id))
|
|
|
|
selectedFontIds.value = filterSetByValidIds(selectedFontIds.value, validIds)
|
|
favoriteFontIds.value = filterSetByValidIds(favoriteFontIds.value, validIds)
|
|
previewFontIds.value = filterSetByValidIds(previewFontIds.value, validIds)
|
|
|
|
writeSet('font.favoriteFontIds', favoriteFontIds.value)
|
|
writeSet('font.previewFontIds', previewFontIds.value)
|
|
|
|
fonts.value = nextFonts.map(font => ({
|
|
...font,
|
|
isFavorite: favoriteFontIds.value.has(font.id),
|
|
}))
|
|
updateFontTree()
|
|
}
|
|
|
|
function removeFont(fontId: string) {
|
|
const index = fonts.value.findIndex(f => f.id === fontId)
|
|
if (index !== -1) {
|
|
fonts.value.splice(index, 1)
|
|
}
|
|
selectedFontIds.value.delete(fontId)
|
|
favoriteFontIds.value.delete(fontId)
|
|
writeSet('font.favoriteFontIds', favoriteFontIds.value)
|
|
previewFontIds.value.delete(fontId)
|
|
writeSet('font.previewFontIds', previewFontIds.value)
|
|
}
|
|
|
|
function selectFont(fontId: string) {
|
|
selectedFontIds.value.add(fontId)
|
|
}
|
|
|
|
function unselectFont(fontId: string) {
|
|
selectedFontIds.value.delete(fontId)
|
|
}
|
|
|
|
function toggleSelectFont(fontId: string) {
|
|
if (selectedFontIds.value.has(fontId)) {
|
|
unselectFont(fontId)
|
|
} else {
|
|
selectFont(fontId)
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedFontIds.value.clear()
|
|
}
|
|
|
|
function favoriteFont(fontId: string) {
|
|
const font = fonts.value.find(f => f.id === fontId)
|
|
if (font) {
|
|
font.isFavorite = true
|
|
favoriteFontIds.value.add(fontId)
|
|
writeSet('font.favoriteFontIds', favoriteFontIds.value)
|
|
}
|
|
}
|
|
|
|
function unfavoriteFont(fontId: string) {
|
|
const font = fonts.value.find(f => f.id === fontId)
|
|
if (font) {
|
|
font.isFavorite = false
|
|
favoriteFontIds.value.delete(fontId)
|
|
writeSet('font.favoriteFontIds', favoriteFontIds.value)
|
|
}
|
|
}
|
|
|
|
function toggleFavorite(fontId: string) {
|
|
if (favoriteFontIds.value.has(fontId)) {
|
|
unfavoriteFont(fontId)
|
|
} else {
|
|
favoriteFont(fontId)
|
|
}
|
|
}
|
|
|
|
function addToPreview(fontId: string) {
|
|
previewFontIds.value.add(fontId)
|
|
writeSet('font.previewFontIds', previewFontIds.value)
|
|
}
|
|
|
|
function removeFromPreview(fontId: string) {
|
|
previewFontIds.value.delete(fontId)
|
|
writeSet('font.previewFontIds', previewFontIds.value)
|
|
}
|
|
|
|
function togglePreview(fontId: string) {
|
|
if (previewFontIds.value.has(fontId)) {
|
|
removeFromPreview(fontId)
|
|
} else {
|
|
addToPreview(fontId)
|
|
}
|
|
}
|
|
|
|
function clearPreview() {
|
|
previewFontIds.value.clear()
|
|
writeSet('font.previewFontIds', previewFontIds.value)
|
|
}
|
|
|
|
function setCategoryExpanded(categoryName: string, expanded: boolean) {
|
|
const next = new Set(expandedCategoryNames.value)
|
|
if (expanded) {
|
|
next.add(categoryName)
|
|
} else {
|
|
next.delete(categoryName)
|
|
}
|
|
expandedCategoryNames.value = next
|
|
writeSet('font.expandedCategories', expandedCategoryNames.value)
|
|
}
|
|
|
|
async function loadFont(fontInfo: FontInfo) {
|
|
if (fontInfo.loaded || !fontInfo.path) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const font = await loadFontWithProgress(fontInfo.path, (progress) => {
|
|
fontInfo.progress = progress
|
|
})
|
|
fontInfo.font = font
|
|
fontInfo.loaded = true
|
|
fontInfo.progress = 100
|
|
} catch (error) {
|
|
console.error(`Failed to load font ${fontInfo.name}:`, error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function buildFontTree(fontList: FontInfo[]): FontTreeNode[] {
|
|
const tree: FontTreeNode[] = []
|
|
const categoryMap = new Map<string, FontTreeNode>()
|
|
|
|
for (const font of fontList) {
|
|
let categoryNode = categoryMap.get(font.category)
|
|
|
|
if (!categoryNode) {
|
|
categoryNode = {
|
|
name: font.category,
|
|
type: 'category',
|
|
children: [],
|
|
expanded: expandedCategoryNames.value.has(font.category),
|
|
selected: false,
|
|
}
|
|
categoryMap.set(font.category, categoryNode)
|
|
tree.push(categoryNode)
|
|
}
|
|
|
|
const fontNode: FontTreeNode = {
|
|
name: font.name,
|
|
type: 'font',
|
|
fontInfo: font,
|
|
expanded: false,
|
|
selected: selectedFontIds.value.has(font.id),
|
|
}
|
|
|
|
categoryNode.children!.push(fontNode)
|
|
}
|
|
|
|
// 按类别名称排序
|
|
tree.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
|
|
|
// 每个类别内的字体按名称排序
|
|
for (const category of tree) {
|
|
if (category.children) {
|
|
category.children.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
|
}
|
|
}
|
|
|
|
return tree
|
|
}
|
|
|
|
function updateFontTree() {
|
|
fontTree.value = buildFontTree(fonts.value)
|
|
}
|
|
|
|
return {
|
|
// 状态
|
|
fonts,
|
|
selectedFontIds,
|
|
favoriteFontIds,
|
|
previewFontIds,
|
|
expandedCategoryNames,
|
|
fontTree,
|
|
isLoadingFonts,
|
|
|
|
// 计算属性
|
|
selectedFonts,
|
|
favoriteFonts,
|
|
previewFonts,
|
|
favoriteTree,
|
|
|
|
// 方法
|
|
addFont,
|
|
replaceFonts,
|
|
removeFont,
|
|
selectFont,
|
|
unselectFont,
|
|
toggleSelectFont,
|
|
clearSelection,
|
|
favoriteFont,
|
|
unfavoriteFont,
|
|
toggleFavorite,
|
|
addToPreview,
|
|
removeFromPreview,
|
|
togglePreview,
|
|
clearPreview,
|
|
setCategoryExpanded,
|
|
loadFont,
|
|
updateFontTree,
|
|
}
|
|
})
|