Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 66 additions & 31 deletions src/library.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf, Modal } from 'obsidian';
import { ItemView, WorkspaceLeaf, Modal, Scope, setIcon, stringifyYaml, Notice } from 'obsidian';
import BeastVault from './main';
import { subTitle, hexToRgb } from './utils';
import { type RawAdversary, AdversaryCard } from './ui';
Expand All @@ -16,11 +16,18 @@ export class LibraryView extends ItemView {
search: undefined
};
private grid: HTMLElement;
private searchInput: HTMLInputElement;

constructor(leaf: WorkspaceLeaf, private plugin: BeastVault) {
super(leaf);
this.icon = 'library';
this.navigation = false;
this.scope = new Scope(this.app.scope);
this.scope.register(['Mod'], 'f', (e) => {
e.preventDefault();
this.searchInput?.focus();
return false;
});
}

everything() {
Expand Down Expand Up @@ -55,17 +62,12 @@ export class LibraryView extends ItemView {
return 'BeastVault Library';
}

// TODO: cannot copy any text from view for some reason
// TODO: add copy button to mini cards
// TODO: when popup active, add right/left navigation
// TODO: refresh on library refresh, default color change
// TODO: ctrl+f to focus on search
// FIXME: for some reason battle points are sometimes displayed
async onOpen() {
this.contentEl.empty();
this.grid = null!;
const controls = this.contentEl.createDiv({ cls: 'bv-fixed' });

const searchInput = controls.createDiv('search-input-container').createEl('input', {
this.searchInput = controls.createDiv('search-input-container').createEl('input', {
attr: {
enterkeyhint: 'search',
type: 'search',
Expand All @@ -74,41 +76,37 @@ export class LibraryView extends ItemView {
}
});

searchInput.addEventListener('input', (event) => {
if (this.filters.search) this.searchInput.value = this.filters.search;

this.searchInput.addEventListener('input', (event) => {
this.filters.search = (event.target as HTMLInputElement).value.trim().toLowerCase() || undefined;
this.renderBlocks();
});

const items = [...this.plugin.allAdversaries(), ...this.plugin.allEnvironments()];

// TODO: instead of hardcoding options, scan library for existing values

const sources = [...new Set(items.map(i => i.source ?? 'homebrew'))].sort();
const sourceDropdown = controls.createEl('select', { cls: 'dropdown' });
sourceDropdown.createEl('option', { text: 'All Sources', value: 'all' });
sourceDropdown.createEl('option', { text: 'Core Rulebook', value: 'corebook' });
sourceDropdown.createEl('option', { text: 'Homebrew', value: 'homebrew' });
for (const source of sources) {
sourceDropdown.createEl('option', { text: source.charAt(0).toUpperCase() + source.slice(1), value: source });
}
sourceDropdown.value = this.filters.source;

sourceDropdown.addEventListener('change', (event) => {
this.filters.source = (event.target as HTMLSelectElement).value;
this.renderBlocks();
});

const types = [...new Set(items.map(i => i.type).filter(Boolean).map(t => t!.toLowerCase().split(/\s/)[0]))].sort();
const typeDropdown = controls.createEl('select', { cls: 'dropdown' });
typeDropdown.createEl('option', { text: 'All Types', value: 'all' });
typeDropdown.createEl('option', { text: 'Adversaries', value: 'adversaries' });
typeDropdown.createEl('option', { text: 'Environments', value: 'environments' });
typeDropdown.createEl('option', { text: 'Minion', value: 'minion' });
typeDropdown.createEl('option', { text: 'Horde', value: 'horde' });
typeDropdown.createEl('option', { text: 'Standard', value: 'standard' });
typeDropdown.createEl('option', { text: 'Skulk', value: 'skulk' });
typeDropdown.createEl('option', { text: 'Leader', value: 'leader' });
typeDropdown.createEl('option', { text: 'Ranged', value: 'ranged' });
typeDropdown.createEl('option', { text: 'Bruiser', value: 'bruiser' });
typeDropdown.createEl('option', { text: 'Solo', value: 'solo' });
typeDropdown.createEl('option', { text: 'Social', value: 'social' });
// typeDropdown.createEl('option', { text: 'Social Environment' }); // TODO
typeDropdown.createEl('option', { text: 'Traversal', value: 'traversal' });
typeDropdown.createEl('option', { text: 'Exploration', value: 'exploration' });
typeDropdown.createEl('option', { text: 'Event', value: 'event' });
for (const type of types) {
typeDropdown.createEl('option', { text: type.charAt(0).toUpperCase() + type.slice(1), value: type });
}
typeDropdown.value = this.filters.type;

typeDropdown.addEventListener('change', (event) => {
this.filters.type = (event.target as HTMLSelectElement).value;
Expand All @@ -117,7 +115,7 @@ export class LibraryView extends ItemView {

const byTier = controls.createDiv({ text: 'Tiers: ' });
for (let i = 1; i <= 4; i++) {
const button = byTier.createEl('button', { cls: 'bv-tier-button bv-inactive', text: `${i}` });
const button = byTier.createEl('button', { cls: `bv-tier-button ${this.filters.tier[i] ? '' : 'bv-inactive'}`, text: `${i}` });
button.addEventListener('click', () => {
button.toggleClass('bv-inactive', this.filters.tier[i]);
this.filters.tier[i] = !this.filters.tier[i];
Expand All @@ -132,27 +130,46 @@ export class LibraryView extends ItemView {
sortDropdown.createEl('option', { text: 'Name', value: 'name' });
sortDropdown.createEl('option', { text: 'Type', value: 'type' });
sortDropdown.createEl('option', { text: 'Source', value: 'source' });
sortDropdown.value = this.sortBy;

sortBy.addEventListener('change', (event) => {
this.sortBy = (event.target as HTMLSelectElement).value as SortBy;
this.renderBlocks();
});

this.renderBlocks();
this.plugin.updateStatusBar();
}

renderBlocks() {
this.grid ??= this.contentEl.createDiv('bv-library');
this.grid.empty();
const everything = this.everything();
for (const item of everything) {
for (const [i, item] of everything.entries()) {
const card = this.grid.createDiv({ cls: 'callout bv-statblock bv-library-item', attr: { 'data-callout': 'daggerheart-card' } });
card.createDiv({ cls: 'callout-title bv-larger' }).createEl('b', { text: item.name });
card.createDiv({ text: subTitle(item.tier, item.type) })
card.createEl('p', { cls: 'bv-smaller bv-muted' }).createEl('i', { text: item.desc || '' })
card.createDiv({ cls: 'bv-source', text: `[${item.source}]` });

const copyBtn = card.createEl('button', {
cls: 'clickable-icon bv-mini-copy',
attr: { 'aria-label': 'Copy to clipboard' }
});
setIcon(copyBtn, 'copy');
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
const copy = { ...item };
copy.id = Math.random().toString(36).slice(2);
delete copy.source;
delete copy.raw;
void navigator.clipboard.writeText(`\`\`\`daggerheart\n${item.raw ? item.raw : stringifyYaml(copy)}\`\`\`\n`);
new Notice('Copied to clipboard');
});

card.addEventListener('click', () => {
new AdversaryPreviewModal(this.plugin, item).open();
if (window.getSelection()?.toString()) return;
new AdversaryPreviewModal(this.plugin, everything, i).open();
})

const color = this.plugin.state.settings.defaultColor;
Expand All @@ -166,9 +183,27 @@ export class LibraryView extends ItemView {
}

class AdversaryPreviewModal extends Modal {
constructor(plugin: BeastVault, adv: RawAdversary) {
private index: number;

constructor(private plugin: BeastVault, private items: RawAdversary[], index: number) {
super(plugin.app);
const card = new AdversaryCard(this.contentEl, adv, plugin, true);
this.index = index;
this.scope.register([], 'ArrowLeft', () => { this.navigate(-1); return false; });
this.scope.register([], 'ArrowRight', () => { this.navigate(1); return false; });
this.renderCard();
}

private navigate(delta: number) {
const newIndex = this.index + delta;
if (newIndex >= 0 && newIndex < this.items.length) {
this.index = newIndex;
this.renderCard();
}
}

private renderCard() {
this.contentEl.empty();
const card = new AdversaryCard(this.contentEl, this.items[this.index], this.plugin, true);
card.render();
}
}
10 changes: 10 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export default class BeastVault extends Plugin {
}
}

this.refreshLibraryViews();
return newLibrary;
}

Expand Down Expand Up @@ -332,6 +333,15 @@ export default class BeastVault extends Plugin {
for (const [block] of this.activeBlocks) {
block.render();
}
for (const leaf of this.app.workspace.getLeavesOfType(LIBRARY_VIEW_TYPE)) {
(leaf.view as LibraryView).renderBlocks();
}
}

refreshLibraryViews() {
for (const leaf of this.app.workspace.getLeavesOfType(LIBRARY_VIEW_TYPE)) {
void (leaf.view as LibraryView).onOpen();
}
}

updateCard(keys: (string | number)[], value: string | number) {
Expand Down
12 changes: 12 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ input[type=checkbox].bv-slot {
margin-top: auto;
}

.bv-mini-copy {
position: absolute;
bottom: var(--size-2-2);
left: var(--size-2-2);
opacity: 0;
transition: opacity 0.1s;
}

.bv-library-item:hover .bv-mini-copy {
opacity: var(--icon-opacity);
}

.bv-inactive {
color: var(--text-faint) !important;
}
Expand Down