Fix history storage and default conversion options
This commit is contained in:
+79
-15
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user