修复导入器字体预览与根目录
修正文件管理器 Home 根路径并恢复本地来源入口 为字体候选增加受 manifest 约束的临时 FontFace 预览 同步字体预览读取约束
This commit is contained in:
@@ -1996,6 +1996,19 @@ pub(crate) fn read_ui_editor_font_bytes(
|
||||
.map(|(_, bytes)| tauri::ipc::Response::new(bytes))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_ui_editor_font_preview(
|
||||
project_path: String,
|
||||
asset_id: String,
|
||||
relative_path: String,
|
||||
) -> Result<tauri::ipc::Response, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "file.read")?;
|
||||
let manifest = read_existing_manifest_for_project(root)?;
|
||||
let asset = find_registered_ui_editor_font(&manifest, asset_id.trim(), relative_path.trim())?;
|
||||
read_registered_ui_editor_font(root, asset).map(|(_, bytes)| tauri::ipc::Response::new(bytes))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn check_ui_editor_font_glyph_coverage(
|
||||
project_path: String,
|
||||
|
||||
@@ -2301,6 +2301,7 @@ fn main() {
|
||||
import_ui_editor_assets,
|
||||
prepare_ui_editor_project_fonts,
|
||||
read_ui_editor_font_bytes,
|
||||
read_ui_editor_font_preview,
|
||||
check_ui_editor_font_glyph_coverage,
|
||||
suggest_ui_design_semantic,
|
||||
recognize_ui,
|
||||
|
||||
@@ -79,12 +79,16 @@ export function AssetImporter({
|
||||
const [files, setFiles] = useState<ManagerFile[]>([]);
|
||||
const [selected, setSelected] = useState<ManagerFile[]>([]);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [fontPreviewFamily, setFontPreviewFamily] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPath, setCurrentPath] = useState(ROOT_PATH);
|
||||
const currentPathRef = useRef(ROOT_PATH);
|
||||
const remoteLoadedRef = useRef(false);
|
||||
const previewScopeIdRef = useRef(createProjectResourcePreviewScopeId());
|
||||
const previewFontFaceRef = useRef<FontFace | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const previousScopeId = previewScopeIdRef.current;
|
||||
@@ -221,15 +225,56 @@ export function AssetImporter({
|
||||
setFiles(buildRootFiles(settings));
|
||||
setSelected([]);
|
||||
setPreview(null);
|
||||
if (previewFontFaceRef.current) {
|
||||
document.fonts?.delete(previewFontFaceRef.current);
|
||||
previewFontFaceRef.current = null;
|
||||
}
|
||||
setFontPreviewFamily(null);
|
||||
void refresh(ROOT_PATH);
|
||||
}, [open, refresh, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
const item = selected.at(-1);
|
||||
if (previewFontFaceRef.current) {
|
||||
document.fonts?.delete(previewFontFaceRef.current);
|
||||
previewFontFaceRef.current = null;
|
||||
}
|
||||
setFontPreviewFamily(null);
|
||||
if (!item || item.isDirectory) {
|
||||
setPreview(null);
|
||||
return;
|
||||
}
|
||||
if (settings.local.localPolicy.destination === 'assets/fonts') {
|
||||
setPreview(null);
|
||||
void (async () => {
|
||||
try {
|
||||
if (
|
||||
typeof FontFace === 'undefined' ||
|
||||
!document.fonts ||
|
||||
!item.asset
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bytes = await invoke<ArrayBuffer>(
|
||||
'read_ui_editor_font_preview',
|
||||
{
|
||||
projectPath,
|
||||
assetId: item.asset.id,
|
||||
relativePath: item.asset.localPath,
|
||||
},
|
||||
);
|
||||
const family = `asset-importer-preview-${item.asset.id}`;
|
||||
const face = new FontFace(family, bytes);
|
||||
await face.load();
|
||||
previewFontFaceRef.current = face;
|
||||
document.fonts.add(face);
|
||||
setFontPreviewFamily(family);
|
||||
} catch {
|
||||
setFontPreviewFamily(null);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (item.previewUrl) {
|
||||
setPreview(item.previewUrl);
|
||||
return;
|
||||
@@ -255,7 +300,16 @@ export function AssetImporter({
|
||||
})
|
||||
.then((value) => setPreview(value.dataUrl))
|
||||
.catch(() => setPreview(null));
|
||||
}, [projectPath, selected]);
|
||||
}, [projectPath, selected, settings.local.localPolicy.destination]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (previewFontFaceRef.current) {
|
||||
document.fonts?.delete(previewFontFaceRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const pickLocal = async () => {
|
||||
setError(null);
|
||||
@@ -361,7 +415,7 @@ export function AssetImporter({
|
||||
void pickLocal();
|
||||
};
|
||||
const handleFolderChange = (path: string) => {
|
||||
const nextPath = path || ROOT_PATH;
|
||||
const nextPath = path === '/' ? ROOT_PATH : path;
|
||||
currentPathRef.current = nextPath;
|
||||
setCurrentPath(nextPath);
|
||||
if (
|
||||
@@ -412,7 +466,7 @@ export function AssetImporter({
|
||||
) : null}
|
||||
<div className="min-h-0 flex-1 p-3">
|
||||
<FileManager
|
||||
key={`${settings.ariaLabel}:${open ? 'open' : 'closed'}`}
|
||||
key={`${settings.ariaLabel}:${currentPath}`}
|
||||
style={{ minHeight: 0, height: '100%', width: '100%' }}
|
||||
files={files}
|
||||
layout="grid"
|
||||
@@ -452,7 +506,14 @@ export function AssetImporter({
|
||||
className="max-h-72 max-w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-(--platform-text-soft)">
|
||||
<span
|
||||
className="text-xs text-(--platform-text-soft)"
|
||||
style={
|
||||
fontPreviewFamily
|
||||
? { fontFamily: fontPreviewFamily, fontSize: '1rem' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{selected.at(-1)?.name ?? '选择一个文件'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ UI Editor 的 `State.font_assets` 正式承载项目字体面资源;`FontAsset
|
||||
|
||||
字体资源发现与 Sprite 保持同一项目资产语义:字体 importer 打开后读取 manifest 与项目文件列表,只展示已登记且具有字体 MIME 或受支持字体扩展名的项目资产;不扫描或接纳未登记文件,不展示云端素材库。从电脑导入使用 Tauri 系统文件选择器,Rust 整批读取普通文件,拒绝符号链接、集合字体、超过 `8 MiB` 的单文件、超过 `64` 个字体面或 `32 MiB` 项目总量,完成真实签名、字体表、名称与 weight/style 解析后复制到 `assets/fonts/` 并登记 manifest。内容相同的字体复用已登记资源;Sprite 与 Font 的 State 批量加入都采用幂等合并:相同 ID 且完整资源相等时跳过,同 ID 数据冲突时整批失败。删除只移除会话 State 资源并清空相应 `Image.target_graphic` 或 `Text.font` 引用,不删除项目文件或 manifest 条目。
|
||||
|
||||
候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。`BestFit` 保持现有取最大字号的近似,不在本次字体闭环中扩展为测量算法。
|
||||
候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。Importer 预览也只读取同一受 manifest 约束的字体字节,再临时加载 `FontFace` 显示选中文件名;切换选择或关闭时立即卸载。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。`BestFit` 保持现有取最大字号的近似,不在本次字体闭环中扩展为测量算法。
|
||||
|
||||
## 2026-08-18 UI Editor 图片 / 字体通用 AssetImporter 契约
|
||||
|
||||
|
||||
Reference in New Issue
Block a user