Files
note2any/src/assets.ts
2025-10-16 14:06:24 +08:00

491 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件assets.ts
* 功能:资源管理(图标 / 静态资源引用 / 动态加载)。
*/
import { App, PluginManifest, Notice, requestUrl, FileSystemAdapter, TAbstractFile, TFile, TFolder } from "obsidian";
import * as zip from "@zip.js/zip.js";
import DefaultTheme from "./default-theme";
import DefaultHighlight from "./default-highlight";
import { NMPSettings } from "./settings";
import { ExpertSettings, defaultExpertSettings, expertSettingsFromString } from "./expert-settings";
export interface Theme {
name: string
className: string
desc: string
author: string
css: string
}
export interface Highlight {
name: string
url: string
css: string
}
export default class AssetsManager {
app: App;
defaultTheme: Theme = DefaultTheme;
manifest: PluginManifest;
themes: Theme[];
highlights: Highlight[];
assetsPath: string;
themesPath: string;
hilightPath: string;
customCSS: string = '';
themeCfg: string;
hilightCfg: string;
customCSSPath: string;
iconsPath: string;
wasmPath: string;
expertSettings: ExpertSettings;
isLoaded: boolean = false;
private loadingPromise: Promise<void> | null = null; // 防止重复并发加载
private static instance: AssetsManager;
// 静态方法,用于获取实例
public static getInstance(): AssetsManager {
if (!AssetsManager.instance) {
AssetsManager.instance = new AssetsManager();
}
return AssetsManager.instance;
}
public static setup(app: App, manifest: PluginManifest) {
AssetsManager.getInstance()._setup(app, manifest);
}
private _setup(app: App, manifest: PluginManifest) {
this.app = app;
this.manifest = manifest;
this.assetsPath = this.app.vault.configDir + '/plugins/' + this.manifest.id + '/assets/';
this.themesPath = this.assetsPath + 'themes/';
this.hilightPath = this.assetsPath + 'highlights/';
this.themeCfg = this.assetsPath + 'themes.json';
this.hilightCfg = this.assetsPath + 'highlights.json';
this.customCSSPath = this.assetsPath + 'custom.css';
this.iconsPath = this.assetsPath + 'icons/';
this.wasmPath = this.assetsPath + 'lib.wasm';
}
private constructor() {
}
async loadAssets() {
if (this.isLoaded) return;
if (this.loadingPromise) {
// 已经在加载中,复用同一个 promise
return this.loadingPromise;
}
console.time('[Assets] loadAssets');
this.loadingPromise = (async () => {
try {
// 并行加载互不依赖的资源,加速启动
await Promise.all([
this.loadThemes().catch(e => console.error('[Assets] loadThemes 失败', e)),
this.loadHighlights().catch(e => console.error('[Assets] loadHighlights 失败', e)),
this.loadCustomCSS().catch(e => console.error('[Assets] loadCustomCSS 失败', e)),
this.loadExpertSettings().catch(e => console.error('[Assets] loadExpertSettings 失败', e)),
]);
this.isLoaded = true;
console.log('[Assets] 资源加载完成', {
themeCount: this.themes?.length ?? 0,
highlightCount: this.highlights?.length ?? 0,
customCSS: this.customCSS?.length ?? 0
});
} finally {
console.timeEnd('[Assets] loadAssets');
this.loadingPromise = null;
}
})();
return this.loadingPromise;
}
async loadThemes() {
try {
console.log('[Assets] loadThemes:start');
if (!await this.app.vault.adapter.exists(this.themeCfg)) {
new Notice('主题资源未下载,请前往设置下载!');
this.themes = [this.defaultTheme];
console.log('[Assets] loadThemes:themes.json missing -> default only');
return;
}
const data = await this.app.vault.adapter.read(this.themeCfg);
if (data) {
const themes = JSON.parse(data);
await this.loadCSS(themes);
this.themes = [this.defaultTheme, ... themes];
console.log('[Assets] loadThemes:done', { count: this.themes.length });
}
} catch (error) {
console.error(error);
new Notice('themes.json解析失败');
}
}
async loadCSS(themes: Theme[]) {
try {
await Promise.all(
themes.map(async (theme) => {
try {
const cssFile = this.themesPath + theme.className + '.css';
const cssContent = await this.app.vault.adapter.read(cssFile);
if (cssContent) {
theme.css = cssContent;
}
} catch (e) {
console.warn('[Assets] 读取主题 CSS 失败', theme.className, e);
}
})
);
} catch (error) {
console.error(error);
new Notice('读取CSS失败');
}
}
async loadCustomCSS() {
try {
console.log('[Assets] loadCustomCSS:start');
const customCSSNote = NMPSettings.getInstance().customCSSNote;
if (customCSSNote != '') {
const file = this.searchFile(customCSSNote);
if (file) {
const cssContent = await this.app.vault.adapter.read(file.path);
if (cssContent) {
this.customCSS = cssContent.replace(/```css/gi, '').replace(/```/g, '');
}
}
else {
new Notice(customCSSNote + '自定义CSS文件不存在');
}
return;
}
if (!await this.app.vault.adapter.exists(this.customCSSPath)) {
return;
}
const cssContent = await this.app.vault.adapter.read(this.customCSSPath);
if (cssContent) {
this.customCSS = cssContent;
}
console.log('[Assets] loadCustomCSS:done', { hasContent: this.customCSS.length > 0 });
} catch (error) {
console.error(error);
new Notice('读取CSS失败');
}
}
async loadExpertSettings() {
try {
console.log('[Assets] loadExpertSettings:start');
const note = NMPSettings.getInstance().expertSettingsNote;
if (note != '') {
const file = this.searchFile(note);
if (file) {
let content = await this.app.vault.adapter.read(file.path);
if (content) {
this.expertSettings = expertSettingsFromString(content);
}
else {
this.expertSettings = defaultExpertSettings;
new Notice(note + '专家设置文件内容为空!');
}
}
else {
this.expertSettings = defaultExpertSettings;
new Notice(note + '专家设置不存在!');
}
}
else {
this.expertSettings = defaultExpertSettings;
}
console.log('[Assets] loadExpertSettings:done');
} catch (error) {
console.error(error);
new Notice('读取专家设置失败!');
}
}
async loadHighlights() {
try {
console.log('[Assets] loadHighlights:start');
const defaultHighlight = {name: '默认', url: '', css: DefaultHighlight};
this.highlights = [defaultHighlight];
if (!await this.app.vault.adapter.exists(this.hilightCfg)) {
new Notice('高亮资源未下载,请前往设置下载!');
console.log('[Assets] loadHighlights:highlights.json missing -> default only');
return;
}
const data = await this.app.vault.adapter.read(this.hilightCfg);
if (data) {
const items = JSON.parse(data);
for (const item of items) {
const cssFile = this.hilightPath + item.name + '.css';
const cssContent = await this.app.vault.adapter.read(cssFile);
this.highlights.push({name: item.name, url: item.url, css: cssContent});
}
console.log('[Assets] loadHighlights:done', { count: this.highlights.length });
}
}
catch (error) {
console.error(error);
new Notice('highlights.json解析失败');
}
}
async loadIcon(name: string) {
const icon = this.iconsPath + name + '.svg';
if (!await this.app.vault.adapter.exists(icon)) {
return '';
}
const iconContent = await this.app.vault.adapter.read(icon);
if (iconContent) {
return iconContent;
}
return '';
}
async loadWasm() {
if (!await this.app.vault.adapter.exists(this.wasmPath)) {
return null;
}
const wasmContent = await this.app.vault.adapter.readBinary(this.wasmPath);
if (wasmContent) {
return wasmContent;
}
return null;
}
getTheme(themeName: string) {
if (themeName === '') {
return this.themes[0];
}
for (const theme of this.themes) {
if (theme.name.toLowerCase() === themeName.toLowerCase() || theme.className.toLowerCase() === themeName.toLowerCase()) {
return theme;
}
}
// 找不到主题时返回第一个主题(默认主题)
console.warn(`[Assets] 主题 "${themeName}" 未找到,使用默认主题`);
return this.themes[0];
}
getHighlight(highlightName: string) {
if (highlightName === '') {
return this.highlights[0];
}
for (const highlight of this.highlights) {
if (highlight.name.toLowerCase() === highlightName.toLowerCase()) {
return highlight;
}
}
// 找不到高亮时返回第一个高亮(默认高亮)
console.warn(`[Assets] 高亮 "${highlightName}" 未找到,使用默认高亮`);
return this.highlights[0];
}
getThemeURL() {
const version = this.manifest.version;
return `https://biboer.cn/gitea/gavin/note2any/releases/download/${version}/assets.zip`;
}
async getStyle() {
const file = this.app.vault.configDir + '/plugins/' + this.manifest.id + '/styles.css';
if (!await this.app.vault.adapter.exists(file)) {
return '';
}
const data = await this.app.vault.adapter.read(file);
if (data) {
return data;
}
return '';
}
async downloadThemes() {
try {
if (await this.app.vault.adapter.exists(this.themeCfg)) {
new Notice('主题资源已存在!')
return;
}
const res = await requestUrl(this.getThemeURL());
const data = res.arrayBuffer;
await this.unzip(new Blob([data]));
await this.loadAssets();
new Notice('主题下载完成!');
} catch (error) {
console.error(error);
await this.removeThemes();
new Notice('主题下载失败, 请检查网络!');
}
}
async unzip(data:Blob) {
const zipFileReader = new zip.BlobReader(data);
const zipReader = new zip.ZipReader(zipFileReader);
const entries = await zipReader.getEntries();
if (!await this.app.vault.adapter.exists(this.assetsPath)) {
await this.app.vault.adapter.mkdir(this.assetsPath);
}
for (const entry of entries) {
if (entry.directory) {
const dirPath = this.assetsPath + entry.filename;
await this.app.vault.adapter.mkdir(dirPath);
}
else {
const filePath = this.assetsPath + entry.filename;
const blobWriter = new zip.Uint8ArrayWriter();
if (entry.getData) {
const data = await entry.getData(blobWriter);
await this.app.vault.adapter.writeBinary(filePath, data.buffer as ArrayBuffer);
}
}
}
await zipReader.close();
}
async removeThemes() {
try {
const adapter = this.app.vault.adapter;
if (await adapter.exists(this.themeCfg)) {
await adapter.remove(this.themeCfg);
}
if (await adapter.exists(this.hilightCfg)) {
await adapter.remove(this.hilightCfg);
}
if (await adapter.exists(this.themesPath)) {
await adapter.rmdir(this.themesPath, true);
}
if (await adapter.exists(this.hilightPath)) {
await adapter.rmdir(this.hilightPath, true);
}
await this.loadAssets();
new Notice('清空完成!');
} catch (error) {
console.error(error);
new Notice('清空主题失败!');
}
}
async openAssets() {
const path = require('path');
const adapter = this.app.vault.adapter as FileSystemAdapter;
const vaultRoot = adapter.getBasePath();
const assets = this.assetsPath;
if (!await adapter.exists(assets)) {
await adapter.mkdir(assets);
}
const dst = path.join(vaultRoot, assets);
const { shell } = require('electron');
shell.openPath(dst);
}
searchFile(nameOrPath: string): TAbstractFile | null {
const resolvedPath = this.resolvePath(nameOrPath);
const vault= this.app.vault;
const attachmentFolderPath = vault.config.attachmentFolderPath || '';
let localPath = resolvedPath;
let file = null;
// 先按路径查找
file = vault.getFileByPath(resolvedPath);
if (file) {
return file;
}
// 在根目录查找
file = vault.getFileByPath(nameOrPath);
if (file) {
return file;
}
// 从附件文件夹查找
if (attachmentFolderPath != '') {
localPath = attachmentFolderPath + '/' + nameOrPath;
file = vault.getFileByPath(localPath)
if (file) {
return file;
}
localPath = attachmentFolderPath + '/' + resolvedPath;
file = vault.getFileByPath(localPath)
if (file) {
return file;
}
}
// 最后查找所有文件,这里只需要判断文件名
const files = vault.getAllLoadedFiles();
for (let f of files) {
if (f instanceof TFolder) continue
file = f as TFile;
if (file.basename === nameOrPath || file.name === nameOrPath) {
return f;
}
}
return null;
}
getResourcePath(path: string): {resUrl:string, filePath:string}|null {
const file = this.searchFile(path) as TFile;
if (file == null) {
return null;
}
const resUrl = this.app.vault.getResourcePath(file);
return {resUrl, filePath: file.path};
}
resolvePath(relativePath: string): string {
const basePath = this.getActiveFileDir();
if (!relativePath.includes('/')) {
return relativePath;
}
const stack = basePath.split("/");
const parts = relativePath.split("/");
stack.pop(); // Remove the current file name (or empty string)
for (const part of parts) {
if (part === ".") continue;
if (part === "..") stack.pop();
else stack.push(part);
}
return stack.join("/");
}
getActiveFileDir() {
const af = this.app.workspace.getActiveFile();
if (af == null) {
return '';
}
const parts = af.path.split('/');
parts.pop();
if (parts.length == 0) {
return '';
}
return parts.join('/');
}
async readFileBinary(path: string) {
const vault= this.app.vault;
const file = this.searchFile(path) as TFile;
if (file == null) {
return null;
}
return await vault.readBinary(file);
}
}