mirror of
https://github.com/alexkurowski/solo-toolkit.git
synced 2026-07-22 10:10:32 +00:00
VTT view
This commit is contained in:
parent
22e2aef4f7
commit
3e7be09dcf
15 changed files with 560 additions and 1 deletions
39
src/main.ts
39
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { Plugin, WorkspaceLeaf } from "obsidian";
|
||||
import { Plugin, TFile, TFolder, WorkspaceLeaf } from "obsidian";
|
||||
import { registerIcons, unregisterIcons } from "./icons";
|
||||
import {
|
||||
SoloToolkitSettingTab,
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
DEFAULT_SETTINGS,
|
||||
} from "./settings";
|
||||
import { SoloToolkitView, VIEW_TYPE } from "./view";
|
||||
import { VttView, VTT_VIEW_TYPE, VTT_EXT } from "./view/vtt";
|
||||
import { soloToolkitExtension } from "./extension";
|
||||
import { soloToolkitPostprocessor } from "./postprocessor";
|
||||
import { exportDeck } from "./utils/deck";
|
||||
|
|
@ -31,6 +32,9 @@ export default class SoloToolkitPlugin extends Plugin {
|
|||
this.registerMarkdownPostProcessor(soloToolkitPostprocessor(this));
|
||||
this.registerEditorExtension(soloToolkitExtension(this));
|
||||
|
||||
this.registerView(VTT_VIEW_TYPE, (leaf) => new VttView(leaf, this));
|
||||
this.registerExtensions([VTT_EXT], VTT_VIEW_TYPE);
|
||||
|
||||
this.addRibbonIcon("srt-ribbon", "Solo RPG Toolkit", () => this.openView());
|
||||
|
||||
this.addCommand({
|
||||
|
|
@ -39,6 +43,12 @@ export default class SoloToolkitPlugin extends Plugin {
|
|||
callback: () => this.openView(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "create-new-vtt",
|
||||
name: "Create new VTT",
|
||||
callback: () => this.createVttFile(),
|
||||
});
|
||||
|
||||
this.addSettingTab(new SoloToolkitSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +76,33 @@ export default class SoloToolkitPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async createVttFile(folder?: TFolder) {
|
||||
const targetFolder = folder
|
||||
? folder
|
||||
: this.app.fileManager.getNewFileParent(
|
||||
this.app.workspace.getActiveFile()?.path || ""
|
||||
);
|
||||
|
||||
try {
|
||||
const file: TFile = await (this.app.fileManager as any).createNewFile(
|
||||
targetFolder,
|
||||
`Untitled.${VTT_EXT}`
|
||||
);
|
||||
|
||||
const leaf = this.app.workspace.getLeaf("tab");
|
||||
await leaf.openFile(file, { active: true });
|
||||
await leaf.setViewState({
|
||||
type: VTT_VIEW_TYPE,
|
||||
state: leaf.view.getState(),
|
||||
});
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(VTT_VIEW_TYPE)[0]
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Error creating vtt:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@
|
|||
@import "./styles/oracle";
|
||||
@import "./styles/plugin";
|
||||
@import "./styles/editor";
|
||||
@import "./styles/vtt";
|
||||
|
|
|
|||
79
src/styles/vtt.scss
Normal file
79
src/styles/vtt.scss
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
.srt-vtt-root {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 12px 12px 32px;
|
||||
background: #111;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.srt-vtt-board {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.srt-vtt-deck {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
.srt-vtt-deck-border {
|
||||
border-color: #fa0;
|
||||
}
|
||||
}
|
||||
|
||||
.srt-vtt-deck-fg {
|
||||
position: relative;
|
||||
background-color: #fafafa;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
width: 96px;
|
||||
height: 136px;
|
||||
}
|
||||
|
||||
.srt-vtt-deck-bg {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 96px;
|
||||
height: 136px;
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.srt-vtt-deck-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -8px;
|
||||
border: 3px solid #444;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.srt-vtt-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fafafa;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border: 3px solid #444;
|
||||
border-radius: 8px;
|
||||
width: 96px;
|
||||
height: 136px;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #fa0;
|
||||
}
|
||||
}
|
||||
33
src/view/vtt/app/board.ts
Normal file
33
src/view/vtt/app/board.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Dnd } from "./dnd";
|
||||
import { Parent, Vec2 } from "./types";
|
||||
import { newVec2 } from "./utils";
|
||||
|
||||
export class Board {
|
||||
dnd: Dnd;
|
||||
position: Vec2 = newVec2();
|
||||
zoom: number = 1;
|
||||
el: HTMLElement;
|
||||
|
||||
constructor(private parent: Parent) {
|
||||
this.dnd = this.parent.dnd;
|
||||
this.el = this.parent.el.createDiv("srt-vtt-board");
|
||||
parent.dnd.makeDraggable(this, {
|
||||
startDragOnParent: true,
|
||||
rightBtn: true,
|
||||
onMove: () => {
|
||||
this.parent.el.style.backgroundPosition = `${this.position.x}px ${this.position.y}px`;
|
||||
},
|
||||
});
|
||||
this.el.parentElement?.addEventListener(
|
||||
"mousewheel",
|
||||
this.handleScroll.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
handleScroll(event: WheelEvent) {
|
||||
this.position.x -= event.deltaX;
|
||||
this.position.y -= event.deltaY;
|
||||
this.parent.el.style.backgroundPosition = `${this.position.x}px ${this.position.y}px`;
|
||||
this.el.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`;
|
||||
}
|
||||
}
|
||||
37
src/view/vtt/app/card.ts
Normal file
37
src/view/vtt/app/card.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { CardId, Parent, Vec2 } from "./types";
|
||||
import { newVec2 } from "./utils";
|
||||
|
||||
export class Card {
|
||||
id: CardId;
|
||||
position: Vec2 = newVec2();
|
||||
image: string;
|
||||
el: HTMLElement;
|
||||
drawn: boolean = false;
|
||||
|
||||
constructor(private parent: Parent, params?: Partial<Card>) {
|
||||
this.el = this.parent.el.createDiv("srt-vtt-card");
|
||||
this.parent.dnd.makeDraggable(this, {
|
||||
onClick: this.hide.bind(this),
|
||||
});
|
||||
if (params) Object.assign(this, params);
|
||||
|
||||
this.el.style.display = "none";
|
||||
this.el.style.backgroundImage = `url(${this.image})`;
|
||||
this.el.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`;
|
||||
}
|
||||
|
||||
draw(at: Vec2) {
|
||||
this.drawn = true;
|
||||
this.position = at;
|
||||
this.el.style.display = "";
|
||||
this.el.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`;
|
||||
this.parent.dnd.moveToTop(this.el);
|
||||
}
|
||||
|
||||
hide(event: MouseEvent) {
|
||||
if (event.shiftKey) {
|
||||
this.drawn = false;
|
||||
this.el.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/view/vtt/app/context.ts
Normal file
33
src/view/vtt/app/context.ts
Normal file
File diff suppressed because one or more lines are too long
35
src/view/vtt/app/deck.ts
Normal file
35
src/view/vtt/app/deck.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { DeckId, Parent, Vec2 } from "./types";
|
||||
import { Card } from "./card";
|
||||
import { newVec2 } from "./utils";
|
||||
|
||||
export class Deck {
|
||||
id: DeckId;
|
||||
position: Vec2 = newVec2();
|
||||
image: string;
|
||||
cards: Card[] = [];
|
||||
el: HTMLElement;
|
||||
|
||||
constructor(private parent: Parent, params?: Partial<Deck>) {
|
||||
this.el = this.parent.el.createDiv("srt-vtt-deck");
|
||||
this.el.createDiv("srt-vtt-deck-bg");
|
||||
this.el.createDiv("srt-vtt-deck-fg");
|
||||
this.el.createDiv("srt-vtt-deck-border");
|
||||
this.parent.dnd.makeDraggable(this, {
|
||||
onClick: this.draw.bind(this),
|
||||
});
|
||||
if (params) Object.assign(this, params);
|
||||
|
||||
this.el.style.backgroundImage = `url(${this.image})`;
|
||||
this.el.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const card = this.cards.find((card) => !card.drawn);
|
||||
if (card) {
|
||||
card.draw({
|
||||
x: this.position.x + 120,
|
||||
y: this.position.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/view/vtt/app/dice.ts
Normal file
3
src/view/vtt/app/dice.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export class Dice {
|
||||
|
||||
}
|
||||
118
src/view/vtt/app/dnd.ts
Normal file
118
src/view/vtt/app/dnd.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { Draggable, DragOptions, Vec2 } from "./types";
|
||||
import { identity, newVec2 } from "./utils";
|
||||
|
||||
const DRAG_START_DISTANCE = 6;
|
||||
const CLICK_TIME_DELTA = 600;
|
||||
|
||||
export class Dnd {
|
||||
private dragging: Draggable | null = null;
|
||||
private dragOptions: DragOptions | null = null;
|
||||
|
||||
private dragStartPosition: Vec2 = newVec2();
|
||||
private dragStartAt: number = 0;
|
||||
private isDragging: boolean = false;
|
||||
|
||||
private mouseMoveHandler: (event: MouseEvent) => void;
|
||||
private mouseUpHandler: (event: MouseEvent) => void;
|
||||
|
||||
constructor() {
|
||||
this.mouseMoveHandler = this.handleMouseMove.bind(this);
|
||||
this.mouseUpHandler = this.handleMouseUp.bind(this);
|
||||
document.addEventListener("mousemove", this.mouseMoveHandler);
|
||||
document.addEventListener("mouseup", this.mouseUpHandler);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
||||
document.removeEventListener("mouseup", this.mouseUpHandler);
|
||||
}
|
||||
|
||||
makeDraggable(draggable: Draggable, options?: DragOptions) {
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (
|
||||
(options?.rightBtn && event.button !== 2) ||
|
||||
(!options?.rightBtn && event.button !== 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
this.dragging = draggable;
|
||||
this.dragOptions = options || null;
|
||||
this.isDragging = false;
|
||||
this.dragStartPosition = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
this.dragStartAt = Date.now();
|
||||
|
||||
if (options?.onDrag) {
|
||||
options.onDrag();
|
||||
}
|
||||
};
|
||||
|
||||
const el = options?.startDragOnParent
|
||||
? draggable.el.parentElement
|
||||
: draggable.el;
|
||||
if (el) {
|
||||
el.addEventListener("mousedown", handleMouseDown);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove(event: MouseEvent) {
|
||||
if (this.dragging) {
|
||||
if (this.isDragging) {
|
||||
const mov = (this.dragOptions?.modifyMovement || identity)({
|
||||
x: event.movementX,
|
||||
y: event.movementY,
|
||||
});
|
||||
this.dragging.position = (this.dragOptions?.modifyPosition || identity)(
|
||||
{
|
||||
x: this.dragging.position.x + mov.x,
|
||||
y: this.dragging.position.y + mov.y,
|
||||
}
|
||||
);
|
||||
this.dragging.el.style.transform = (
|
||||
this.dragOptions?.modifyTransform || identity
|
||||
)(
|
||||
`translate(${this.dragging.position.x}px, ${this.dragging.position.y}px)`
|
||||
);
|
||||
|
||||
if (this.dragOptions?.onMove) {
|
||||
this.dragOptions.onMove();
|
||||
}
|
||||
} else {
|
||||
const distance = Math.sqrt(
|
||||
(event.clientX - this.dragStartPosition.x) ** 2 +
|
||||
(event.clientY - this.dragStartPosition.y) ** 2
|
||||
);
|
||||
if (distance >= DRAG_START_DISTANCE) {
|
||||
this.isDragging = true;
|
||||
this.moveToTop(this.dragging.el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp(event: MouseEvent) {
|
||||
if (this.isDragging) {
|
||||
if (this.dragOptions?.onDrop) {
|
||||
this.dragOptions.onDrop();
|
||||
}
|
||||
} else {
|
||||
if (Date.now() - this.dragStartAt <= CLICK_TIME_DELTA) {
|
||||
if (this.dragOptions?.onClick) {
|
||||
this.dragOptions.onClick(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.dragging = null;
|
||||
this.dragOptions = null;
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
moveToTop(el: HTMLElement) {
|
||||
el.parentElement?.appendChild(el);
|
||||
}
|
||||
}
|
||||
1
src/view/vtt/app/index.ts
Normal file
1
src/view/vtt/app/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./main";
|
||||
64
src/view/vtt/app/main.ts
Normal file
64
src/view/vtt/app/main.ts
Normal file
File diff suppressed because one or more lines are too long
33
src/view/vtt/app/types.ts
Normal file
33
src/view/vtt/app/types.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Dnd } from "./dnd";
|
||||
|
||||
export type Vec2 = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export interface Draggable {
|
||||
position: Vec2;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
export interface DragOptions {
|
||||
modifyMovement?: (nextPosition: Vec2) => Vec2;
|
||||
modifyPosition?: (nextPosition: Vec2) => Vec2;
|
||||
modifyTransform?: (nextTransform: string) => string;
|
||||
onDrag?: () => void;
|
||||
onMove?: () => void;
|
||||
onDrop?: () => void;
|
||||
onClick?: (event?: MouseEvent) => void;
|
||||
startDragOnParent?: boolean;
|
||||
rightBtn?: boolean;
|
||||
}
|
||||
|
||||
export interface Parent {
|
||||
dnd: Dnd;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
export type CardId = `c${string}`;
|
||||
export type DeckId = `d${string}`;
|
||||
export type DiceId = `r${string}`;
|
||||
export type IdType = CardId | DeckId | DiceId;
|
||||
11
src/view/vtt/app/utils.ts
Normal file
11
src/view/vtt/app/utils.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { IdType, Vec2 } from "./types";
|
||||
|
||||
const generateString = () => Math.random().toString(36).substring(2, 8);
|
||||
export const generateId = <Id extends IdType>(type: Id): `${Id}${string}` =>
|
||||
`${type}${generateString()}${generateString()}`;
|
||||
|
||||
export const identity = <T>(value: T): T => value;
|
||||
|
||||
export const newVec2 = (x: number = 0, y: number = 0): Vec2 => {
|
||||
return { x, y };
|
||||
};
|
||||
1
src/view/vtt/index.ts
Normal file
1
src/view/vtt/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { VttView, VTT_VIEW_TYPE, VTT_EXT } from "./view";
|
||||
73
src/view/vtt/view.ts
Normal file
73
src/view/vtt/view.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { TextFileView, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import VttPlugin from "src/main";
|
||||
import { VttApp } from "./app";
|
||||
|
||||
export const VTT_VIEW_TYPE = "vtt";
|
||||
export const VTT_EXT = "vtt";
|
||||
|
||||
export class VttView extends TextFileView {
|
||||
plugin: VttPlugin;
|
||||
root: VttApp;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: VttPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async onLoadFile(file: TFile) {
|
||||
this.file = file;
|
||||
this.render(file);
|
||||
}
|
||||
|
||||
async onUnloadFile(_file: TFile) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
getViewData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
setViewData(data: string, clear?: boolean) {
|
||||
this.data = data;
|
||||
if (clear) {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return VTT_VIEW_TYPE;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setViewData("");
|
||||
this.root?.unrender();
|
||||
}
|
||||
|
||||
getContainer() {
|
||||
return this.containerEl.children[1];
|
||||
}
|
||||
|
||||
async render(file: TFile) {
|
||||
const fileData = await this.app.vault.read(file);
|
||||
this.setViewData(fileData);
|
||||
|
||||
const container = this.getContainer();
|
||||
container.classList.add("srt-vtt-root");
|
||||
|
||||
if (this.root) {
|
||||
this.root.update(fileData);
|
||||
} else {
|
||||
this.root = new VttApp(container, fileData);
|
||||
}
|
||||
|
||||
this.root.render();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue