@@ -0,0 +1,35 @@
|
||||
export type AiConfig = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
imageModel: string;
|
||||
textModel: string;
|
||||
models: string[];
|
||||
quality: string;
|
||||
size: string;
|
||||
count: string;
|
||||
};
|
||||
|
||||
export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store";
|
||||
|
||||
export const defaultConfig: AiConfig = {
|
||||
baseUrl: "https://api.openai.com",
|
||||
apiKey: "",
|
||||
model: "gpt-image-2",
|
||||
imageModel: "gpt-image-2",
|
||||
textModel: "gpt-5.5",
|
||||
models: [],
|
||||
quality: "auto",
|
||||
size: "1:1",
|
||||
count: "1",
|
||||
};
|
||||
|
||||
export function normalizeBaseUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function buildApiUrl(baseUrl: string, path: string) {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
const apiBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
|
||||
return `${apiBaseUrl}${path}`;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export type CanvasColorTheme = "light" | "dark";
|
||||
export type CanvasBackgroundMode = "dots" | "lines" | "blank";
|
||||
|
||||
export const canvasThemes = {
|
||||
light: {
|
||||
canvas: {
|
||||
background: "#f4f2ed",
|
||||
dot: "rgba(68,64,60,.28)",
|
||||
line: "rgba(68,64,60,.12)",
|
||||
selectionStroke: "#1c1917",
|
||||
selectionFill: "rgba(28,25,23,.06)",
|
||||
},
|
||||
node: {
|
||||
label: "#57534e",
|
||||
fill: "#e7e5df",
|
||||
panel: "#fbfaf7",
|
||||
stroke: "#d6d3ca",
|
||||
activeStroke: "#1c1917",
|
||||
placeholder: "#8a8479",
|
||||
text: "#292524",
|
||||
muted: "#78716c",
|
||||
faint: "#a8a29e",
|
||||
},
|
||||
toolbar: {
|
||||
panel: "rgba(251,250,247,.96)",
|
||||
border: "#d6d3ca",
|
||||
item: "#57534e",
|
||||
itemHover: "#e7e5df",
|
||||
activeBg: "#e7e5df",
|
||||
activeText: "#292524",
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
canvas: {
|
||||
background: "#181715",
|
||||
dot: "rgba(245,245,244,.24)",
|
||||
line: "rgba(245,245,244,.10)",
|
||||
selectionStroke: "#fafaf9",
|
||||
selectionFill: "rgba(250,250,249,.10)",
|
||||
},
|
||||
node: {
|
||||
label: "#d6d3d1",
|
||||
fill: "#292524",
|
||||
panel: "#1f1d1a",
|
||||
stroke: "#44403c",
|
||||
activeStroke: "#fafaf9",
|
||||
placeholder: "#a8a29e",
|
||||
text: "#f5f5f4",
|
||||
muted: "#d6d3d1",
|
||||
faint: "#78716c",
|
||||
},
|
||||
toolbar: {
|
||||
panel: "rgba(31,29,26,.96)",
|
||||
border: "#44403c",
|
||||
item: "#d6d3d1",
|
||||
itemHover: "#292524",
|
||||
activeBg: "#3a3631",
|
||||
activeText: "#f5f5f4",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type CanvasTheme = (typeof canvasThemes)[CanvasColorTheme];
|
||||
@@ -0,0 +1,3 @@
|
||||
export function createId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
|
||||
export function formatBytes(bytes: number) {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return "";
|
||||
}
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number) {
|
||||
const value = Math.max(0, Math.floor(ms / 1000));
|
||||
const minutes = Math.floor(value / 60);
|
||||
const seconds = value % 60;
|
||||
return minutes ? `${minutes}分${String(seconds).padStart(2, "0")}秒` : `${seconds}秒`;
|
||||
}
|
||||
|
||||
export function getDataUrlByteSize(dataUrl: string) {
|
||||
const base64 = dataUrl.split(",", 2)[1];
|
||||
if (!base64) {
|
||||
return 0;
|
||||
}
|
||||
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
||||
return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
|
||||
}
|
||||
|
||||
export function readFileAsDataUrl(file: File) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ""));
|
||||
reader.onerror = () => reject(new Error("读取图片失败"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export function readImageMeta(dataUrl: string) {
|
||||
return new Promise<{ width: number; height: number; mimeType: string }>((resolve) => {
|
||||
const image = new Image();
|
||||
const done = () => resolve({ width: image.naturalWidth || 1024, height: image.naturalHeight || 1024, mimeType: dataUrl.match(/^data:([^;]+)/)?.[1] || "image/png" });
|
||||
image.onload = done;
|
||||
image.onerror = done;
|
||||
setTimeout(done, 3000);
|
||||
image.src = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export function dataUrlToFile(image: ReferenceImage) {
|
||||
const [header, content] = image.dataUrl.split(",", 2);
|
||||
const mimeType = header.match(/data:(.*?);base64/)?.[1] || image.type || "image/png";
|
||||
const binary = atob(content || "");
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return new File([bytes], image.name || "reference.png", { type: mimeType });
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import localforage from "localforage";
|
||||
import type { StateStorage } from "zustand/middleware";
|
||||
|
||||
localforage.config({
|
||||
name: "infinite-canvas",
|
||||
storeName: "app_state",
|
||||
});
|
||||
|
||||
export const localForageStorage: StateStorage = {
|
||||
getItem: async (name) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
return (await localforage.getItem<string>(name)) || null;
|
||||
} catch {
|
||||
return window.localStorage.getItem(name);
|
||||
}
|
||||
},
|
||||
setItem: async (name, value) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
await localforage.setItem(name, value);
|
||||
} catch {
|
||||
window.localStorage.setItem(name, value);
|
||||
}
|
||||
},
|
||||
removeItem: async (name) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
await localforage.removeItem(name);
|
||||
} catch {
|
||||
window.localStorage.removeItem(name);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FileText, ImagePlus, Images, Maximize2 } from "lucide-react";
|
||||
|
||||
export const navigationTools = [
|
||||
{
|
||||
slug: "canvas",
|
||||
label: "我的画布",
|
||||
icon: Maximize2,
|
||||
},
|
||||
{
|
||||
slug: "image",
|
||||
label: "生图工作台",
|
||||
icon: ImagePlus,
|
||||
},
|
||||
{
|
||||
slug: "prompts",
|
||||
label: "提示词库",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
slug: "assets",
|
||||
label: "我的素材",
|
||||
icon: Images,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type NavigationToolSlug = (typeof navigationTools)[number]["slug"];
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user