refactor: split source code into modules

This commit is contained in:
Janis Pritzkau 2025-03-28 20:24:30 +01:00
parent f8d96f264a
commit 8f069516ac
9 changed files with 318 additions and 331 deletions

View file

@ -2,20 +2,19 @@ import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
@ -31,7 +30,8 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",

304
main.ts
View file

@ -1,304 +0,0 @@
import type { ModelViewerElement } from "@google/model-viewer";
import {
App,
Component,
FileView,
Plugin,
PluginSettingTab,
Setting,
type TFile,
type WorkspaceLeaf,
} from "obsidian";
if (customElements.get("model-viewer") == null) {
import("@google/model-viewer");
}
interface ModelViewerSettings {
cameraControls: boolean;
disablePan: boolean;
disableZoom: boolean;
autoRotate: boolean;
interactionPrompt: boolean;
autoplay: boolean;
}
const DEFAULT_SETTINGS: ModelViewerSettings = {
cameraControls: true,
disablePan: false,
disableZoom: false,
autoRotate: false,
interactionPrompt: false,
autoplay: false,
};
export default class ModelViewerPlugin extends Plugin {
settings: ModelViewerSettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new ModelViewerSettingTab(this.app, this));
this.registerView("model-viewer-file-view", (leaf) => {
return new ModelViewerFileView(leaf, this);
});
this.registerExtensions(["glb", "gltf"], "model-viewer-file-view");
// @ts-expect-error
this.app.embedRegistry.registerExtensions(
["gltf", "glb"],
(context: { containerEl: HTMLElement; linktext?: string }, file: TFile) => {
let params: URLSearchParams;
try {
params = new URLSearchParams(context.linktext?.match(/#(.+)$/)?.[1]);
} catch {
params = new URLSearchParams();
}
return new ModelViewerComponent(this, context.containerEl, file, params);
}
);
}
onunload() {
// @ts-expect-error
this.app.embedRegistry.unregisterExtensions(["gltf", "glb"]);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class ModelViewerComponent extends Component {
plugin: ModelViewerPlugin;
containerEl: HTMLElement;
file: TFile;
viewer: ModelViewerElement;
params: URLSearchParams;
constructor(
plugin: ModelViewerPlugin,
containerEl: HTMLElement,
file: TFile,
params: URLSearchParams
) {
super();
this.plugin = plugin;
this.containerEl = containerEl;
this.file = file;
containerEl.addClass("model-viewer-embed");
this.updateWidth();
this.observer = new MutationObserver(() => {
this.updateWidth();
});
this.observer.observe(this.containerEl, {
attributes: true,
attributeFilter: ["width"],
});
this.params = params;
}
onunload(): void {
this.observer.disconnect();
this.viewer?.remove();
}
observer: MutationObserver;
unload(): void {
this.observer.disconnect();
}
updateWidth() {
const width = this.containerEl.getAttribute("width");
this.containerEl.style.width = width ? `${width}px` : "100%";
}
loadFile() {
this.viewer = document.createElement("model-viewer");
this.viewer.src = this.plugin.app.vault.getResourcePath(this.file);
this.containerEl.appendChild(this.viewer);
const height = this.params.get("height");
if (height) {
this.viewer.style.height = height + "px";
this.viewer.style.maxHeight = "unset";
this.params.delete("height");
}
const aspect = this.params.get("aspect");
if (aspect) {
this.viewer.style.aspectRatio = aspect;
this.params.delete("aspect");
}
const settings = this.plugin.settings;
if (settings.cameraControls) this.viewer.cameraControls = true;
if (settings.disablePan) this.viewer.disablePan = true;
if (settings.disableZoom) this.viewer.disableZoom = true;
if (settings.autoRotate) this.viewer.autoRotate = true;
if (!settings.interactionPrompt) this.viewer.interactionPrompt = "none";
if (settings.autoplay) this.viewer.autoplay = true;
for (const [key, value] of this.params.entries()) {
if (value == "false") {
this.viewer.removeAttribute(key);
} else {
this.viewer.setAttribute(key, value);
}
}
this.viewer.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
e.stopPropagation();
},
{ passive: false }
);
}
}
class ModelViewerFileView extends FileView {
private plugin: ModelViewerPlugin;
constructor(leaf: WorkspaceLeaf, plugin: ModelViewerPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return "model-viewer-file-view";
}
canAcceptExtension(extension: string): boolean {
return ["glb", "gltf"].includes(extension);
}
async onLoadFile(file: TFile): Promise<void> {
this.displayInEl(file);
}
displayInEl(file: TFile) {
const viewer = document.createElement("model-viewer");
viewer.style.position = "absolute";
viewer.style.width = "100%";
viewer.style.height = "100%";
viewer.src = this.app.vault.getResourcePath(file);
this.contentEl.appendChild(viewer);
const settings = this.plugin.settings;
if (settings.cameraControls) viewer.cameraControls = true;
if (settings.disablePan) viewer.disablePan = true;
if (settings.disableZoom) viewer.disableZoom = true;
if (settings.autoRotate) viewer.autoRotate = true;
if (!settings.interactionPrompt) viewer.interactionPrompt = "none";
if (settings.autoplay) viewer.autoplay = true;
viewer.fieldOfView = "50deg";
viewer.maxFieldOfView = "75deg";
viewer.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
e.stopPropagation();
},
{ passive: false }
);
}
async onUnloadFile(file: TFile): Promise<void> {
this.contentEl.empty();
}
}
class ModelViewerSettingTab extends PluginSettingTab {
plugin: ModelViewerPlugin;
constructor(app: App, plugin: ModelViewerPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Camera Controls")
.setDesc("Enable camera controls for the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.cameraControls).onChange(async (value) => {
this.plugin.settings.cameraControls = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Disable Pan")
.setDesc("Disable panning the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disablePan).onChange(async (value) => {
this.plugin.settings.disablePan = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Disable Zoom")
.setDesc("Disable zooming the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableZoom).onChange(async (value) => {
this.plugin.settings.disableZoom = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Auto Rotate")
.setDesc("Enable auto rotation of the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoRotate).onChange(async (value) => {
this.plugin.settings.autoRotate = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Interaction Prompt")
.setDesc("Show prompt to interact with the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.interactionPrompt).onChange(async (value) => {
this.plugin.settings.interactionPrompt = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Autoplay")
.setDesc("Enable autoplay of the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoplay).onChange(async (value) => {
this.plugin.settings.autoplay = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Reset Settings")
.setDesc("Reset settings to default")
.addButton((button) =>
button
.setButtonText("Reset to default")
.setWarning()
.onClick(async () => {
this.plugin.settings = DEFAULT_SETTINGS;
await this.plugin.saveSettings();
this.display();
})
);
}
}

77
src/ModelViewerEmbed.ts Normal file
View file

@ -0,0 +1,77 @@
import { Component, type TFile } from "obsidian";
import type ModelViewerPlugin from "./main";
import type { ModelViewerElement } from "@google/model-viewer";
import { createModelViewer } from "./utils/createModelViewer";
export class ModelViewerEmbed extends Component {
plugin: ModelViewerPlugin;
containerEl: HTMLElement;
file: TFile;
viewer?: ModelViewerElement;
params: URLSearchParams;
constructor(
plugin: ModelViewerPlugin,
containerEl: HTMLElement,
file: TFile,
params: URLSearchParams
) {
super();
this.plugin = plugin;
this.containerEl = containerEl;
this.file = file;
containerEl.addClass("model-viewer-embed");
this.updateWidth();
this.observer = new MutationObserver(() => {
this.updateWidth();
});
this.observer.observe(this.containerEl, {
attributes: true,
attributeFilter: ["width"],
});
this.params = params;
}
onunload(): void {
this.observer.disconnect();
this.viewer?.remove();
}
observer: MutationObserver;
unload(): void {
this.observer.disconnect();
}
updateWidth() {
const width = this.containerEl.getAttribute("width");
this.containerEl.style.width = width ? `${width}px` : "100%";
}
loadFile() {
this.viewer = createModelViewer(this.plugin.app, this.file, this.plugin.settings);
this.containerEl.appendChild(this.viewer);
const height = this.params.get("height");
if (height) {
this.viewer.style.height = height + "px";
this.viewer.style.maxHeight = "unset";
this.params.delete("height");
}
const aspect = this.params.get("aspect");
if (aspect) {
this.viewer.style.aspectRatio = aspect;
this.params.delete("aspect");
}
for (const [key, value] of this.params.entries()) {
if (value == "false") {
this.viewer.removeAttribute(key);
} else {
this.viewer.setAttribute(key, value);
}
}
}
}

View file

@ -0,0 +1,41 @@
import { FileView, type TFile, type WorkspaceLeaf } from "obsidian";
import type ModelViewerPlugin from "./main";
import { createModelViewer } from "./utils/createModelViewer";
export class ModelViewerFileView extends FileView {
private plugin: ModelViewerPlugin;
constructor(leaf: WorkspaceLeaf, plugin: ModelViewerPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return "model-viewer-file-view";
}
canAcceptExtension(extension: string): boolean {
return ["glb", "gltf"].includes(extension);
}
async onLoadFile(file: TFile): Promise<void> {
this.displayInEl(file);
}
displayInEl(file: TFile) {
const viewer = createModelViewer(this.app, file, this.plugin.settings);
viewer.fieldOfView = "50deg";
viewer.maxFieldOfView = "75deg";
viewer.style.position = "absolute";
viewer.style.width = "100%";
viewer.style.height = "100%";
this.contentEl.appendChild(viewer);
}
async onUnloadFile(file: TFile): Promise<void> {
this.contentEl.empty();
}
}

View file

@ -0,0 +1,86 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import type ModelViewerPlugin from "./main";
import { DEFAULT_SETTINGS } from "./settings";
export class ModelViewerSettingTab extends PluginSettingTab {
plugin: ModelViewerPlugin;
constructor(app: App, plugin: ModelViewerPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Camera Controls")
.setDesc("Enable camera controls for the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.cameraControls).onChange(async (value) => {
this.plugin.settings.cameraControls = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Disable Pan")
.setDesc("Disable panning the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disablePan).onChange(async (value) => {
this.plugin.settings.disablePan = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Disable Zoom")
.setDesc("Disable zooming the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableZoom).onChange(async (value) => {
this.plugin.settings.disableZoom = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Auto Rotate")
.setDesc("Enable auto rotation of the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoRotate).onChange(async (value) => {
this.plugin.settings.autoRotate = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Interaction Prompt")
.setDesc("Show prompt to interact with the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.interactionPrompt).onChange(async (value) => {
this.plugin.settings.interactionPrompt = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Autoplay")
.setDesc("Enable autoplay of the model")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoplay).onChange(async (value) => {
this.plugin.settings.autoplay = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Reset Settings")
.setDesc("Reset settings to default")
.addButton((button) =>
button
.setButtonText("Reset to default")
.setWarning()
.onClick(async () => {
this.plugin.settings = DEFAULT_SETTINGS;
await this.plugin.saveSettings();
this.display();
})
);
}
}

52
src/main.ts Normal file
View file

@ -0,0 +1,52 @@
import { Plugin, type TFile } from "obsidian";
import { ModelViewerEmbed } from "./ModelViewerEmbed";
import { ModelViewerFileView } from "./ModelViewerFileView";
import { ModelViewerSettingTab } from "./ModelViewerSettingTab";
import { DEFAULT_SETTINGS, type ModelViewerSettings } from "./settings";
if (customElements.get("model-viewer") == null) {
import("@google/model-viewer");
}
export default class ModelViewerPlugin extends Plugin {
settings!: ModelViewerSettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new ModelViewerSettingTab(this.app, this));
this.registerView("model-viewer-file-view", (leaf) => {
return new ModelViewerFileView(leaf, this);
});
this.registerExtensions(["glb", "gltf"], "model-viewer-file-view");
// @ts-expect-error
this.app.embedRegistry.registerExtensions(
["gltf", "glb"],
(context: { containerEl: HTMLElement; linktext?: string }, file: TFile) => {
let params: URLSearchParams;
try {
params = new URLSearchParams(context.linktext?.match(/#(.+)$/)?.[1]);
} catch {
params = new URLSearchParams();
}
return new ModelViewerEmbed(this, context.containerEl, file, params);
}
);
}
onunload() {
// @ts-expect-error
this.app.embedRegistry.unregisterExtensions(["gltf", "glb"]);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}

17
src/settings.ts Normal file
View file

@ -0,0 +1,17 @@
export interface ModelViewerSettings {
cameraControls: boolean;
disablePan: boolean;
disableZoom: boolean;
autoRotate: boolean;
interactionPrompt: boolean;
autoplay: boolean;
}
export const DEFAULT_SETTINGS: ModelViewerSettings = {
cameraControls: true,
disablePan: false,
disableZoom: false,
autoRotate: false,
interactionPrompt: false,
autoplay: false,
};

View file

@ -0,0 +1,25 @@
import type { App, TFile } from "obsidian";
import type { ModelViewerSettings } from "../settings";
export function createModelViewer(app: App, file: TFile, settings: ModelViewerSettings) {
const viewer = document.createElement("model-viewer");
viewer.src = app.vault.getResourcePath(file);
if (settings.cameraControls) viewer.cameraControls = true;
if (settings.disablePan) viewer.disablePan = true;
if (settings.disableZoom) viewer.disableZoom = true;
if (settings.autoRotate) viewer.autoRotate = true;
if (!settings.interactionPrompt) viewer.interactionPrompt = "none";
if (settings.autoplay) viewer.autoplay = true;
viewer.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
e.stopPropagation();
},
{ passive: false }
);
return viewer;
}

View file

@ -1,24 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strict": true,
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"]
}