Fix history storage and default conversion options

This commit is contained in:
wuyanwanwu
2026-08-14 01:13:23 +08:00
parent 552ed5e6fa
commit a5d4850728
12 changed files with 93 additions and 29 deletions
+79 -15
View File
@@ -60,6 +60,45 @@ type SavedPattern = {
};
const TRANSPARENT_ALPHA_THRESHOLD = 128;
const HISTORY_LIMIT = 30;
const HISTORY_STORAGE_KEY = "bead-pattern-history";
const HISTORY_DB_NAME = "pixel-bead-pattern-history";
const HISTORY_STORE_NAME = "patterns";
function openHistoryDatabase() {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(HISTORY_DB_NAME, 1);
request.onupgradeneeded = () => {
if (!request.result.objectStoreNames.contains(HISTORY_STORE_NAME)) {
request.result.createObjectStore(HISTORY_STORE_NAME);
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function readPatternHistory(): Promise<SavedPattern[]> {
const database = await openHistoryDatabase();
return new Promise((resolve, reject) => {
const transaction = database.transaction(HISTORY_STORE_NAME, "readonly");
const request = transaction.objectStore(HISTORY_STORE_NAME).get(HISTORY_STORAGE_KEY);
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
request.onerror = () => reject(request.error);
transaction.oncomplete = () => database.close();
});
}
async function writePatternHistory(value: SavedPattern[]) {
const database = await openHistoryDatabase();
return new Promise<void>((resolve, reject) => {
const transaction = database.transaction(HISTORY_STORE_NAME, "readwrite");
transaction.objectStore(HISTORY_STORE_NAME).put(value, HISTORY_STORAGE_KEY);
transaction.oncomplete = () => { database.close(); resolve(); };
transaction.onerror = () => { database.close(); reject(transaction.error); };
transaction.onabort = () => { database.close(); reject(transaction.error); };
});
}
const MARD_SERIES_NAMES: Record<string, string> = {
A: "黄橙系", B: "绿色系", C: "蓝青系", D: "紫蓝系", E: "粉红系",
@@ -405,8 +444,8 @@ export default function Home() {
const [gridWidth, setGridWidth] = useState(100);
const [gridHeight, setGridHeight] = useState(100);
const [colorLimit, setColorLimit] = useState(64);
const [fitMode, setFitMode] = useState<"cover" | "contain">("cover");
const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("smooth");
const [fitMode, setFitMode] = useState<"cover" | "contain">("contain");
const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("dominant");
const [cropZoom, setCropZoom] = useState(1);
const [cropX, setCropX] = useState(0);
const [cropY, setCropY] = useState(0);
@@ -440,11 +479,26 @@ export default function Home() {
useEffect(() => {
logUsage({ event: "page_view" });
const saved = localStorage.getItem("bead-pattern-history");
if (!saved) return;
queueMicrotask(() => {
try { setHistory(JSON.parse(saved)); } catch { localStorage.removeItem("bead-pattern-history"); }
});
let cancelled = false;
const loadHistory = async () => {
try {
let saved = await readPatternHistory();
const legacy = localStorage.getItem(HISTORY_STORAGE_KEY);
if (saved.length === 0 && legacy) {
const parsed = JSON.parse(legacy);
if (Array.isArray(parsed)) {
saved = parsed.slice(0, HISTORY_LIMIT);
await writePatternHistory(saved);
}
localStorage.removeItem(HISTORY_STORAGE_KEY);
}
if (!cancelled) setHistory(saved.slice(0, HISTORY_LIMIT));
} catch {
if (!cancelled) setStatus("历史记录读取失败,请检查浏览器是否允许本地存储");
}
};
void loadHistory();
return () => { cancelled = true; };
}, []);
const convertImage = (
@@ -775,12 +829,12 @@ export default function Home() {
setHoveredPixel({ row: row + 1, column: column + 1, color: pixel.color, left: event.clientX - rect.left + 14, top: event.clientY - rect.top + 14 });
};
const persistHistory = (next: SavedPattern[]) => {
const persistHistory = async (next: SavedPattern[]) => {
await writePatternHistory(next);
setHistory(next);
localStorage.setItem("bead-pattern-history", JSON.stringify(next));
};
const savePattern = () => {
const savePattern = async () => {
if (!pixels.length) return;
const entry: SavedPattern = {
id: crypto.randomUUID(),
@@ -791,8 +845,12 @@ export default function Home() {
colorLimit,
codes: pixels.map((pixel) => pixel.color?.code ?? null),
};
persistHistory([entry, ...history].slice(0, 30));
setStatus("图纸已保存到本机历史记录");
try {
await persistHistory([entry, ...history].slice(0, HISTORY_LIMIT));
setStatus("图纸已保存到本机历史记录");
} catch {
setStatus("保存历史失败,请检查浏览器是否允许本地存储或剩余空间是否充足");
}
};
const restorePattern = (entry: SavedPattern) => {
@@ -809,7 +867,13 @@ export default function Home() {
document.getElementById("pattern-preview")?.scrollIntoView({ behavior: "smooth" });
};
const removePattern = (id: string) => persistHistory(history.filter((entry) => entry.id !== id));
const removePattern = async (id: string) => {
try {
await persistHistory(history.filter((entry) => entry.id !== id));
} catch {
setStatus("删除历史失败,请稍后重试");
}
};
const filteredHistory = useMemo(() => {
const key = historyQuery.trim().toLowerCase();
@@ -870,8 +934,8 @@ export default function Home() {
</div>
<label className="field"><span>使 <b>{colorLimit}</b></span><input type="range" min="2" max="64" value={colorLimit} onChange={(e) => setColorLimit(Number(e.target.value))} /></label>
<label className="field"><span></span><select value={samplingStrategy} onChange={(e) => setSamplingStrategy(e.target.value as SamplingStrategy)}><option value="smooth"></option><option value="dominant"></option></select></label>
<label className="field"><span></span><select value={fitMode} onChange={(e) => setFitMode(e.target.value as "cover" | "contain")}><option value="cover"></option><option value="contain"></option></select></label>
<label className="field"><span></span><select value={samplingStrategy} onChange={(e) => setSamplingStrategy(e.target.value as SamplingStrategy)}><option value="dominant"></option><option value="smooth"></option></select></label>
<label className="field"><span></span><select value={fitMode} onChange={(e) => setFitMode(e.target.value as "cover" | "contain")}><option value="contain"></option><option value="cover"></option></select></label>
<button className="primary-button" onClick={() => convertImage()}></button>
<p className="privacy-note"></p>
</aside>