obsidian#ItemView TypeScript Examples

The following examples show how to use obsidian#ItemView. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ViewLoader.ts    From obsidian-rss with GNU General Public License v3.0 6 votes vote down vote up
export default class ViewLoader extends ItemView {
    private feed: FolderView;
    private readonly plugin: RssReaderPlugin;


    constructor(leaf: WorkspaceLeaf, plugin: RssReaderPlugin) {
        super(leaf);
        this.plugin = plugin;
    }

    getDisplayText(): string {
        return t("RSS_Feeds");
    }

    getViewType(): string {
        return VIEW_ID;
    }

    getIcon(): string {
        return "rss";
    }

    protected async onOpen(): Promise<void> {
        this.feed = new FolderView({
            target: (this as any).contentEl,
            props: {
                plugin: this.plugin
            },

        });
    }

    protected onClose(): Promise<void> {
        if(this.feed) {
            this.feed.$destroy();
        }
        return Promise.resolve();
    }
}
Example #2
Source File: view.ts    From better-word-count with MIT License 6 votes vote down vote up
export default class StatsView extends ItemView {
  private statistics: Statistics;

  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType(): string {
    return VIEW_TYPE_STATS;
  }

  getDisplayText(): string {
    return "Statistics";
  }

  getIcon(): string {
    return STATS_ICON_NAME;
  }

  async onOpen(): Promise<void> {
    this.statistics = new Statistics({
      target: (this as any).contentEl,
    });
  }
}
Example #3
Source File: dictionaryView.ts    From obsidian-dictionary with GNU Affero General Public License v3.0 5 votes vote down vote up
export default class DictionaryView extends ItemView {

    plugin: DictionaryPlugin;
    private _view: DictionaryComponent;

    constructor(leaf: WorkspaceLeaf, plugin: DictionaryPlugin) {
        super(leaf);
        this.plugin = plugin;
    }

    query(query: string): void {
        dispatchEvent(new CustomEvent("obsidian-dictionary-plugin-search", { detail: { query } }));
    }

    getViewType(): string {
        return VIEW_TYPE;
    }

    getDisplayText(): string {
        return VIEW_DISPLAY_TEXT;
    }

    getIcon(): string {
        return VIEW_ICON;
    }

    onClose(): Promise<void> {
        this._view.$destroy();
        return super.onClose();
    }

    onOpen(): Promise<void> {
        this._view = new DictionaryComponent({
            target: this.contentEl,
            props: {
                manager: this.plugin.manager,
                localDictionary: this.plugin.localDictionary,
            }
        });
        this.contentEl.addClass("dictionary-view-content");
        addEventListener('dictionary-open-language-switcher', () => {
            new LanguageChooser(this.app, this.plugin).open();
        });
        addEventListener('dictionary-open-api-switcher', () => {
            new DefinitionProviderChooser(this.app, this.plugin).open();
        });
        return super.onOpen();
    }

}
Example #4
Source File: view.ts    From obsidian-initiative-tracker with GNU General Public License v3.0 5 votes vote down vote up
export class CreatureView extends ItemView {
    buttonEl = this.contentEl.createDiv("creature-view-button");
    statblockEl = this.contentEl.createDiv("creature-statblock-container");
    constructor(leaf: WorkspaceLeaf, public plugin: InitiativeTracker) {
        super(leaf);
        this.load();
        this.containerEl.addClass("creature-view-container");
    }
    onload() {
        new ExtraButtonComponent(this.buttonEl)
            .setIcon("cross")
            .setTooltip("Close Statblock")
            .onClick(() => {
                this.render();
                this.app.workspace.trigger("initiative-tracker:stop-viewing");
            });
    }
    onunload(): void {
        this.app.workspace.trigger("initiative-tracker:stop-viewing");
    }
    render(creature?: HomebrewCreature) {
        this.statblockEl.empty();
        if (!creature) {
            this.statblockEl.createEl("em", {
                text: "Select a creature to view it here."
            });
            return;
        }
        if (
            this.plugin.canUseStatBlocks &&
            this.plugin.statblockVersion?.major >= 2
        ) {
            const statblock = this.plugin.statblocks.render(
                creature,
                this.statblockEl,
                creature.display
            );
            if (statblock) {
                this.addChild(statblock);
            }
        } else {
            this.statblockEl.createEl("em", {
                text: "Install the TTRPG Statblocks plugin to use this feature!"
            });
        }
    }
    getDisplayText(): string {
        return "Combatant";
    }
    getIcon(): string {
        return CREATURE;
    }
    getViewType(): string {
        return CREATURE_TRACKER_VIEW;
    }
}
Example #5
Source File: view.ts    From obsidian-checklist-plugin with MIT License 4 votes vote down vote up
export default class TodoListView extends ItemView {
  private _app: App
  private lastRerender = 0
  private groupedItems: TodoGroup[] = []
  private itemsByFile = new Map<string, TodoItem[]>()
  private initialLoad = true
  private searchTerm = ""

  constructor(leaf: WorkspaceLeaf, private plugin: TodoPlugin) {
    super(leaf)
  }

  getViewType(): string {
    return TODO_VIEW_TYPE
  }

  getDisplayText(): string {
    return "Todo List"
  }

  getIcon(): string {
    return "checkmark"
  }

  get todoTagArray() {
    return this.plugin
      .getSettingValue("todoPageName")
      .trim()
      .split("\n")
      .map((e) => e.toLowerCase())
      .filter((e) => e)
  }

  get visibleTodoTagArray() {
    return this.todoTagArray.filter((t) => !this.plugin.getSettingValue("_hiddenTags").includes(t))
  }

  async onClose() {
    this._app.$destroy()
  }

  async onOpen(): Promise<void> {
    this._app = new App({
      target: (this as any).contentEl,
      props: this.props(),
    })
    this.registerEvent(
      this.app.metadataCache.on("resolved", async () => {
        if (!this.plugin.getSettingValue("autoRefresh") && !this.initialLoad) return
        if (this.initialLoad) this.initialLoad = false
        await this.refresh()
      })
    )
    this.registerEvent(this.app.vault.on("delete", (file) => this.deleteFile(file.path)))
  }

  async refresh(all = false) {
    if (all) {
      this.lastRerender = 0
      this.itemsByFile.clear()
    }
    await this.calculateAllItems()
    this.groupItems()
    this.renderView()
    this.lastRerender = +new Date()
  }

  rerender() {
    this.renderView()
  }

  private deleteFile(path: string) {
    this.itemsByFile.delete(path)
    this.groupItems()
    this.renderView()
  }

  private props() {
    return {
      todoTags: this.todoTagArray,
      lookAndFeel: this.plugin.getSettingValue("lookAndFeel"),
      subGroups: this.plugin.getSettingValue("subGroups"),
      _collapsedSections: this.plugin.getSettingValue("_collapsedSections"),
      _hiddenTags: this.plugin.getSettingValue("_hiddenTags"),
      app: this.app,
      todoGroups: this.groupedItems,
      initialLoad: this.initialLoad,
      updateSetting: (updates: Partial<TodoSettings>) => this.plugin.updateSettings(updates),
      onSearch: (val: string) => {
        this.searchTerm = val
        this.refresh()
      },
    }
  }

  private async calculateAllItems() {
    const items = await parseTodos(
      this.app.vault.getFiles(),
      this.todoTagArray.length === 0 ? ["*"] : this.visibleTodoTagArray,
      this.app.metadataCache,
      this.app.vault,
      this.plugin.getSettingValue("includeFiles"),
      this.plugin.getSettingValue("showChecked"),
      this.lastRerender
    )
    const changesMap = new Map<string, TodoItem[]>()
    for (const item of items) {
      if (!changesMap.has(item.filePath)) changesMap.set(item.filePath, [])
      changesMap.get(item.filePath).push(item)
    }
    for (const [path, pathItems] of changesMap) this.itemsByFile.set(path, pathItems)
  }

  private groupItems() {
    const flattenedItems = Array.from(this.itemsByFile.values()).flat()
    const searchedItems = flattenedItems.filter((e) => e.originalText.toLowerCase().includes(this.searchTerm))
    this.groupedItems = groupTodos(
      searchedItems,
      this.plugin.getSettingValue("groupBy"),
      this.plugin.getSettingValue("sortDirectionGroups"),
      this.plugin.getSettingValue("sortDirectionItems"),
      this.plugin.getSettingValue("subGroups"),
      this.plugin.getSettingValue("sortDirectionSubGroups")
    )
  }

  private renderView() {
    this._app.$set(this.props())
  }
}
Example #6
Source File: mapView.ts    From obsidian-map-view with GNU General Public License v3.0 4 votes vote down vote up
export class MapView extends ItemView {
    private settings: PluginSettings;
    /** The displayed controls and objects of the map, separated from its logical state.
     * Must only be updated in updateMarkersToState */
    private state: MapState;
    /** The state that was last saved to Obsidian's history stack */
    private lastSavedState: MapState;
    /** The map data */
    private display = new (class {
        /** The HTML element holding the map */
        mapDiv: HTMLDivElement;
        /** The leaflet map instance */
        map: leaflet.Map;
        tileLayer: leaflet.TileLayer;
        /** The cluster management class */
        clusterGroup: leaflet.MarkerClusterGroup;
        /** The markers currently on the map */
        markers: MarkersMap = new Map();
        controls: ViewControls;
        /** The search controls (search & clear buttons) */
        searchControls: SearchControl = null;
        /** A marker of the last search result */
        searchResult: leaflet.Marker = null;
    })();
    private ongoingChanges = 0;
    private plugin: MapViewPlugin;
    /** The default state as saved in the plugin settings */
    private defaultState: MapState;
    /**
     * The Workspace Leaf that a note was last opened in.
     * This is saved so the same leaf can be reused when opening subsequent notes, making the flow consistent & predictable for the user.
     */
    private newPaneLeaf: WorkspaceLeaf;
    /** Is the view currently open */
    private isOpen: boolean = false;

    /**
     * Construct a new map instance
     * @param leaf The leaf the map should be put in
     * @param settings The plugin settings
     * @param plugin The plugin instance
     */
    constructor(
        leaf: WorkspaceLeaf,
        settings: PluginSettings,
        plugin: MapViewPlugin
    ) {
        super(leaf);
        this.navigation = true;
        this.settings = settings;
        this.plugin = plugin;
        // Create the default state by the configuration
        this.defaultState = this.settings.defaultState;

        // Listen to file changes so we can update markers accordingly
        this.app.vault.on('delete', (file) =>
            this.updateMarkersWithRelationToFile(file.path, null, true)
        );
        this.app.metadataCache.on('changed', (file) =>
            this.updateMarkersWithRelationToFile(file.path, file, false)
        );
        // On rename we don't need to do anything because the markers hold a TFile, and the TFile object doesn't change
        // when the file name changes. Only its internal path field changes accordingly.
        // this.app.vault.on('rename', (file, oldPath) => this.updateMarkersWithRelationToFile(oldPath, file, true));
        this.app.workspace.on('css-change', () => {
            console.log('Map view: map refresh due to CSS change');
            this.refreshMap();
        });
        this.app.workspace.on('file-open', async (file: TFile) => {
            if (this.getState().followActiveNote && file) {
                let viewState = this.leaf?.getViewState();
                if (viewState) {
                    let mapState = viewState.state as MapState;
                    const newQuery = `path:"${file.path}"`;
                    // Change the map state only if the file has actually changed. If the user just went back
                    // and forth and the map is still focused on the same file, don't ruin the user's possible
                    // zoom and pan
                    if (mapState.query != newQuery) {
                        mapState.query = newQuery;
                        await this.setViewState(mapState, true, true);
                    }
                }
            }
        });
    }

    onMoreOptionsMenu(menu: Menu) {
        menu.addItem((item: MenuItem) => {
            item.setTitle('Copy Map View URL').onClick(() => {
                this.copyStateUrl();
            });
        });
        super.onMoreOptionsMenu(menu);
    }

    copyStateUrl() {
        const params = stateToUrl(this.state);
        const url = `obsidian://mapview?action=open&${params}`;
        navigator.clipboard.writeText(url);
        new Notice('Copied state URL to clipboard');
    }

    getMarkers() {
        return this.display.markers;
    }

    /**
     * This is the native Obsidian setState method.
     * You should *not* call it directly, but rather through this.leaf.setViewState(state), which will
     * take care of preserving the Obsidian history if required.
     */
    async setState(state: MapState, result: any) {
        // If there are ongoing changes to the map happening at once, don't bother updating the state -- it will only
        // cause unexpected jumps and flickers
        if (this.ongoingChanges > 0) return;
        // It can go below 0 in some cases
        this.ongoingChanges = 0;
        if (this.shouldSaveToHistory(state)) {
            result.history = true;
            this.lastSavedState = copyState(state);
        }
        await this.setViewState(state, true, false);
        if (this.display.controls) this.display.controls.tryToGuessPreset();
    }

    getState() {
        return this.state;
    }

    /** Decides and returns true if the given state change, compared to the last saved state, is substantial
     * enough to be saved as an Obsidian history state */
    shouldSaveToHistory(newState: MapState) {
        if (!this.settings.saveHistory) return false;
        if (!this.lastSavedState) return true;
        if (newState.forceHistorySave) {
            newState.forceHistorySave = false;
            return true;
        }
        // If the zoom changed by HISTORY_SAVE_ZOOM_DIFF -- save the history
        if (
            Math.abs(newState.mapZoom - this.lastSavedState.mapZoom) >=
            consts.HISTORY_SAVE_ZOOM_DIFF
        )
            return true;
        // If the previous center is no longer visible -- save the history
        // (this is partially cheating because we use the actual map and not the state object)
        if (
            this.lastSavedState.mapCenter &&
            !this.display.map
                .getBounds()
                .contains(this.lastSavedState.mapCenter)
        )
            return true;
        if (
            newState.query != this.lastSavedState.query ||
            newState.chosenMapSource != this.lastSavedState.chosenMapSource
        )
            return true;
        return false;
    }

    getViewType() {
        return 'map';
    }
    getDisplayText() {
        return 'Interactive Map View';
    }

    isDarkMode(settings: PluginSettings): boolean {
        if (settings.chosenMapMode === 'dark') return true;
        if (settings.chosenMapMode === 'light') return false;
        // Auto mode - check if the theme is dark
        if ((this.app.vault as any).getConfig('theme') === 'obsidian')
            return true;
        return false;
    }

    async onOpen() {
        this.isOpen = true;
        this.state = this.defaultState;
        this.display.controls = new ViewControls(
            this.contentEl,
            this.settings,
            this.app,
            this,
            this.plugin
        );
        this.contentEl.style.padding = '0px 0px';
        this.display.controls.createControls();
        this.display.mapDiv = createDiv(
            { cls: 'map' },
            (el: HTMLDivElement) => {
                el.style.zIndex = '1';
                el.style.width = '100%';
                el.style.height = '100%';
            }
        );
        this.contentEl.append(this.display.mapDiv);
        // Make touch move nicer on mobile
        this.contentEl.addEventListener('touchmove', (ev) => {
            ev.stopPropagation();
        });
        await this.createMap();
        return super.onOpen();
    }

    onClose() {
        this.isOpen = false;
        return super.onClose();
    }

    onResize() {
        this.display.map.invalidateSize();
    }

    /**
     * This internal method of setting the state will NOT register the change to the Obsidian
     * history stack. If you want that, use `this.leaf.setViewState(state)` instead.
     */
    public async setViewState(
        state: MapState,
        updateControls: boolean = false,
        considerAutoFit: boolean = false
    ) {
        if (state) {
            const newState = mergeStates(this.state, state);
            this.updateTileLayerByState(newState);
            await this.updateMarkersToState(newState);
            if (considerAutoFit && this.settings.autoZoom)
                await this.autoFitMapToMarkers();
            if (updateControls) this.display.controls.updateControlsToState();
        }
    }

    public getMapSource(): TileSource {
        return this.settings.mapSources[this.state.chosenMapSource];
    }

    updateTileLayerByState(newState: MapState) {
        if (
            this.display.tileLayer &&
            this.state.chosenMapSource != newState.chosenMapSource
        ) {
            this.display.tileLayer.remove();
            this.display.tileLayer = null;
        }
        this.state.chosenMapSource = newState.chosenMapSource;
        if (!this.display.tileLayer) {
            const isDark = this.isDarkMode(this.settings);
            const chosenMapSource = this.getMapSource();
            const attribution =
                chosenMapSource.urlLight ===
                DEFAULT_SETTINGS.mapSources[0].urlLight
                    ? '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                    : '';
            let revertMap = false;
            let mapSourceUrl = chosenMapSource.urlLight;
            if (isDark) {
                if (chosenMapSource.urlDark)
                    mapSourceUrl = chosenMapSource.urlDark;
                else revertMap = true;
            }
            const neededClassName = revertMap ? 'dark-mode' : '';
            const maxNativeZoom =
                chosenMapSource.maxZoom ?? consts.DEFAULT_MAX_TILE_ZOOM;
            this.display.tileLayer = new leaflet.TileLayer(mapSourceUrl, {
                maxZoom: this.settings.letZoomBeyondMax
                    ? consts.MAX_ZOOM
                    : maxNativeZoom,
                maxNativeZoom: maxNativeZoom,
                subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
                attribution: attribution,
                className: neededClassName,
            });
            this.display.map.addLayer(this.display.tileLayer);

            if (!chosenMapSource?.ignoreErrors) {
                let recentTileError = false;
                this.display.tileLayer.on(
                    'tileerror',
                    (event: leaflet.TileErrorEvent) => {
                        if (!recentTileError) {
                            new Notice(
                                `Map view: unable to load map tiles. Try switching the map source using the View controls.`,
                                20000
                            );
                            recentTileError = true;
                            setTimeout(() => {
                                recentTileError = false;
                            }, 5000);
                        }
                    }
                );
            }
        }
    }

    async refreshMap() {
        this.display?.tileLayer?.remove();
        this.display.tileLayer = null;
        this.display?.map?.off();
        this.display?.map?.remove();
        this.display?.markers?.clear();
        this.display?.controls?.controlsDiv?.remove();
        this.display.controls?.reload();
        await this.createMap();
        this.updateMarkersToState(this.state, true);
        this.display.controls.updateControlsToState();
    }

	/*
	 * Receive a partial object of fields to change and calls the Obsidian setViewState
	 * method to set a history state.
	*/
	async changeViewAndSaveHistory(partialState: Partial<MapState>) {
		// This check is seemingly a duplicate of the one inside setViewState, but it's
		// actually very needed. Without it, it's possible that we'd call Obsidian's
		// setViewState (the one below) with the same object twice, in the first call
		// (which may have ongoingChanges > 0) we'll ignore the change and in the 2nd call
		// Obsidian will ignore the change (thinking the state didn't change).
		// We want to ensure setViewState is called only if we mean to change the state
		if (this.ongoingChanges > 0)
			return;
		const viewState = this.leaf?.getViewState();
		if (viewState?.state) {
			const newState = Object.assign({}, viewState?.state, partialState);
			await this.leaf.setViewState({...viewState, state: newState});
		}
	}

    async createMap() {
        // LeafletJS compatability: disable tree-shaking for the full-screen module
        var dummy = leafletFullscreen;
        this.display.map = new leaflet.Map(this.display.mapDiv, {
            center: this.defaultState.mapCenter,
            zoom: this.defaultState.mapZoom,
            zoomControl: false,
            worldCopyJump: true,
            maxBoundsViscosity: 1.0,
        });
        leaflet.control
            .zoom({
                position: 'topright',
            })
            .addTo(this.display.map);
        this.updateTileLayerByState(this.state);
        this.display.clusterGroup = new leaflet.MarkerClusterGroup({
            maxClusterRadius:
                this.settings.maxClusterRadiusPixels ??
                DEFAULT_SETTINGS.maxClusterRadiusPixels,
            animate: false,
        });
        this.display.map.addLayer(this.display.clusterGroup);

        this.display.map.on('zoomend', async (event: leaflet.LeafletEvent) => {
            this.ongoingChanges -= 1;
			await this.changeViewAndSaveHistory({
				mapZoom: this.display.map.getZoom(),
				mapCenter: this.display.map.getCenter()
			});
            this.display?.controls?.invalidateActivePreset();
        });
        this.display.map.on('moveend', async (event: leaflet.LeafletEvent) => {
            this.ongoingChanges -= 1;
			await this.changeViewAndSaveHistory({
				mapZoom: this.display.map.getZoom(),
				mapCenter: this.display.map.getCenter()
			});
            this.display?.controls?.invalidateActivePreset();
        });
        this.display.map.on('movestart', (event: leaflet.LeafletEvent) => {
            this.ongoingChanges += 1;
        });
        this.display.map.on('zoomstart', (event: leaflet.LeafletEvent) => {
            this.ongoingChanges += 1;
        });
        this.display.map.on(
            'doubleClickZoom',
            (event: leaflet.LeafletEvent) => {
                this.ongoingChanges += 1;
            }
        );

        this.display.searchControls = new SearchControl(
            { position: 'topright' },
            this,
            this.app,
            this.settings
        );
        this.display.map.addControl(this.display.searchControls);

        if (this.settings.showClusterPreview) {
            this.display.clusterGroup.on('clustermouseover', (cluster) => {
                let content = this.contentEl.createDiv();
                content.classList.add('clusterPreviewContainer');
                for (const m of cluster.propagatedFrom.getAllChildMarkers()) {
                    const marker = m as leaflet.Marker;
                    const iconElement = marker.options.icon.createIcon();
                    iconElement.classList.add('clusterPreviewIcon');
                    content.appendChild(iconElement);
                    if (
                        content.children.length >=
                        consts.MAX_CLUSTER_PREVIEW_ICONS
                    )
                        break;
                }
                cluster.propagatedFrom
                    .bindPopup(content, {
                        closeButton: true,
                        autoPan: false,
                        className: 'marker-popup',
                    })
                    .openPopup();
                cluster.propagatedFrom.activePopup = content;
            });
            this.display.clusterGroup.on('clustermouseout', (cluster) => {
                cluster.propagatedFrom.closePopup();
            });
        }

        // Build the map marker right-click context menu
        this.display.map.on(
            'contextmenu',
            async (event: leaflet.LeafletMouseEvent) => {
                let mapPopup = new Menu(this.app);
                mapPopup.setNoIcon();
                mapPopup.addItem((item: MenuItem) => {
                    const location = `${event.latlng.lat},${event.latlng.lng}`;
                    item.setTitle('New note here (inline)');
                    item.onClick(async (ev) => {
                        const newFileName = utils.formatWithTemplates(
                            this.settings.newNoteNameFormat
                        );
                        const file: TFile = await utils.newNote(
                            this.app,
                            'multiLocation',
                            this.settings.newNotePath,
                            newFileName,
                            location,
                            this.settings.newNoteTemplate
                        );
                        this.goToFile(
                            file,
                            ev.ctrlKey,
                            utils.handleNewNoteCursorMarker
                        );
                    });
                });
                mapPopup.addItem((item: MenuItem) => {
                    const location = `${event.latlng.lat},${event.latlng.lng}`;
                    item.setTitle('New note here (front matter)');
                    item.onClick(async (ev) => {
                        const newFileName = utils.formatWithTemplates(
                            this.settings.newNoteNameFormat
                        );
                        const file: TFile = await utils.newNote(
                            this.app,
                            'singleLocation',
                            this.settings.newNotePath,
                            newFileName,
                            location,
                            this.settings.newNoteTemplate
                        );
                        this.goToFile(
                            file,
                            ev.ctrlKey,
                            utils.handleNewNoteCursorMarker
                        );
                    });
                });
                mapPopup.addItem((item: MenuItem) => {
                    const location = `${event.latlng.lat},${event.latlng.lng}`;
                    item.setTitle(`Copy geolocation`);
                    item.onClick((_ev) => {
                        navigator.clipboard.writeText(`[](geo:${location})`);
                    });
                });
                mapPopup.addItem((item: MenuItem) => {
                    const location = `${event.latlng.lat},${event.latlng.lng}`;
                    item.setTitle(`Copy geolocation as front matter`);
                    item.onClick((_ev) => {
                        navigator.clipboard.writeText(
                            `---\nlocation: [${location}]\n---\n\n`
                        );
                    });
                });
                mapPopup.addItem((item: MenuItem) => {
                    item.setTitle('Open in default app');
                    item.onClick((_ev) => {
                        open(`geo:${event.latlng.lat},${event.latlng.lng}`);
                    });
                });
                utils.populateOpenInItems(
                    mapPopup,
                    event.latlng,
                    this.settings
                );
                mapPopup.showAtPosition(event.originalEvent);
            }
        );
    }

    /**
     * Set the map state
     * @param state The map state to set
     * @param force Force setting the state. Will ignore if the state is old
     */
    async updateMarkersToState(state: MapState, force: boolean = false) {
        if (this.settings.debug) console.time('updateMarkersToState');
        let files = this.app.vault.getFiles();
        // Build the markers and filter them according to the query
        let newMarkers = await buildMarkers(files, this.settings, this.app);
        try {
            newMarkers = this.filterMarkers(newMarkers, state.query);
            state.queryError = false;
        } catch (e) {
            newMarkers = [];
            state.queryError = true;
        }
        finalizeMarkers(newMarkers, this.settings);
        this.state = state;
        this.updateMapMarkers(newMarkers);
        if (
            this.display.map.getCenter().distanceTo(this.state.mapCenter) > 1 ||
            this.display.map.getZoom() != this.state.mapZoom
        ) {
            // We want to call setView only if there was an actual change, because even the tiniest (epsilon) change can
            // cause Leaflet to think it's worth triggering map center change callbacks
            this.display.map.setView(this.state.mapCenter, this.state.mapZoom, {
                animate: false,
                duration: 0,
            });
        }
        this.display.controls.setQueryBoxErrorByState();
        if (this.settings.debug) console.timeEnd('updateMarkersToState');
    }

    filterMarkers(allMarkers: FileMarker[], queryString: string) {
        let results: FileMarker[] = [];
        const query = new Query(this.app, queryString);
        for (const marker of allMarkers)
            if (query.testMarker(marker)) results.push(marker);
        return results;
    }

    /**
     * Update the actual Leaflet markers of the map according to a new list of logical markers.
     * Unchanged markers are not touched, new markers are created and old markers that are not in the updated list are removed.
     * @param newMarkers The new array of FileMarkers
     */
    updateMapMarkers(newMarkers: FileMarker[]) {
        let newMarkersMap: MarkersMap = new Map();
        let markersToAdd: leaflet.Marker[] = [];
        let markersToRemove: leaflet.Marker[] = [];
        for (let marker of newMarkers) {
            const existingMarker = this.display.markers.has(marker.id)
                ? this.display.markers.get(marker.id)
                : null;
            if (existingMarker && existingMarker.isSame(marker)) {
                // This marker exists, so just keep it
                newMarkersMap.set(
                    marker.id,
                    this.display.markers.get(marker.id)
                );
                this.display.markers.delete(marker.id);
            } else {
                // New marker - create it
                marker.mapMarker = this.newLeafletMarker(marker);
                markersToAdd.push(marker.mapMarker);
                newMarkersMap.set(marker.id, marker);
            }
        }
        for (let [key, value] of this.display.markers) {
            markersToRemove.push(value.mapMarker);
        }
        this.display.clusterGroup.removeLayers(markersToRemove);
        this.display.clusterGroup.addLayers(markersToAdd);
        this.display.markers = newMarkersMap;
    }

    private newLeafletMarker(marker: FileMarker): leaflet.Marker {
        let newMarker = leaflet.marker(marker.location, {
            icon: marker.icon || new leaflet.Icon.Default(),
        });
        newMarker.on('click', (event: leaflet.LeafletMouseEvent) => {
            this.goToMarker(marker, event.originalEvent.ctrlKey, true);
        });
        newMarker.on('mouseover', (event: leaflet.LeafletMouseEvent) => {
            if (this.settings.showNotePreview) {
                const previewDetails = {
                    scroll: marker.fileLine,
                    line: marker.fileLine,
                    startLoc: {
                        line: marker.fileLine,
                        col: 0,
                        offset: marker.fileLocation,
                    } as Loc,
                    endLoc: {
                        line: marker.fileLine,
                        col: 0,
                        offset: marker.fileLocation,
                    } as Loc,
                };
                this.app.workspace.trigger(
                    'link-hover',
                    newMarker.getElement(),
                    newMarker.getElement(),
                    marker.file.path,
                    '',
                    previewDetails
                );
            }
            if (this.settings.showNoteNamePopup) {
                const fileName = marker.file.name;
                const fileNameWithoutExtension = fileName.endsWith('.md')
                    ? fileName.substr(0, fileName.lastIndexOf('.md'))
                    : fileName;
                let content = `<p class="map-view-marker-name">${fileNameWithoutExtension}</p>`;
                newMarker
                    .bindPopup(content, {
                        closeButton: true,
                        autoPan: false,
                        className: 'marker-popup',
                    })
                    .openPopup();
            }
        });
        newMarker.on('mouseout', (event: leaflet.LeafletMouseEvent) => {
            newMarker.closePopup();
        });
        newMarker.on('add', (event: leaflet.LeafletEvent) => {
            newMarker
                .getElement()
                .addEventListener('contextmenu', (ev: MouseEvent) => {
                    let mapPopup = new Menu(this.app);
                    mapPopup.setNoIcon();
                    mapPopup.addItem((item: MenuItem) => {
                        item.setTitle('Open note');
                        item.onClick(async (ev) => {
                            this.goToMarker(marker, ev.ctrlKey, true);
                        });
                    });
                    mapPopup.addItem((item: MenuItem) => {
                        item.setTitle('Open geolocation in default app');
                        item.onClick((ev) => {
                            open(
                                `geo:${marker.location.lat},${marker.location.lng}`
                            );
                        });
                    });
                    utils.populateOpenInItems(
                        mapPopup,
                        marker.location,
                        this.settings
                    );
                    mapPopup.showAtPosition(ev);
                    ev.stopPropagation();
                });
        });
        return newMarker;
    }

    /** Zoom the map to fit all markers on the screen */
    public async autoFitMapToMarkers() {
        if (this.display.markers.size > 0) {
            const locations: leaflet.LatLng[] = Array.from(
                this.display.markers.values()
            ).map((fileMarker) => fileMarker.location);
            this.display.map.fitBounds(leaflet.latLngBounds(locations), {
                maxZoom: Math.min(
                    this.settings.zoomOnGoFromNote,
                    this.getMapSource().maxZoom ?? consts.DEFAULT_MAX_TILE_ZOOM
                ),
            });
        }
    }

    /**
     * Open a file in an editor window
     * @param file The file object to open
     * @param useCtrlKeyBehavior If true will use the alternative behaviour, as set in the settings
     * @param editorAction Optional callback to run when the file is opened
     */
    async goToFile(
        file: TFile,
        useCtrlKeyBehavior: boolean,
        editorAction?: (editor: Editor) => Promise<void>
    ) {
        let leafToUse = this.app.workspace.activeLeaf;
        const defaultDifferentPane =
            this.settings.markerClickBehavior != 'samePane';
        // Having a pane to reuse means that we previously opened a note in a new pane and that pane still exists (wasn't closed)
        const havePaneToReuse =
            this.newPaneLeaf &&
            this.newPaneLeaf.view &&
            this.settings.markerClickBehavior != 'alwaysNew';
        if (
            havePaneToReuse ||
            (defaultDifferentPane && !useCtrlKeyBehavior) ||
            (!defaultDifferentPane && useCtrlKeyBehavior)
        ) {
            // We were instructed to use a different pane for opening the note.
            // We go here in the following cases:
            // 1. An existing pane to reuse exists (the user previously opened it, with or without Ctrl).
            //    In this case we use the pane regardless of the default or of Ctrl, assuming that if a user opened a pane
            //    once, she wants to retain it until it's closed. (I hope no one will treat this as a bug...)
            // 2. The default is to use a different pane and Ctrl is not pressed.
            // 3. The default is to NOT use a different pane and Ctrl IS pressed.
            const someOpenMarkdownLeaf =
                this.app.workspace.getLeavesOfType('markdown');
            if (havePaneToReuse) {
                // We have an existing pane, that pane still has a view (it was not closed), and the settings say
                // to use a 2nd pane. That's the only case on which we reuse a pane
                this.app.workspace.setActiveLeaf(this.newPaneLeaf);
                leafToUse = this.newPaneLeaf;
            } else if (
                someOpenMarkdownLeaf.length > 0 &&
                this.settings.markerClickBehavior != 'alwaysNew'
            ) {
                // We don't have a pane to reuse but the user wants a new pane and there is currently an open
                // Markdown pane. Let's take control over it and hope it's the right thing to do
                this.app.workspace.setActiveLeaf(someOpenMarkdownLeaf[0]);
                leafToUse = someOpenMarkdownLeaf[0];
                this.newPaneLeaf = leafToUse;
            } else {
                // We need a new pane. We split it the way the settings tell us
                this.newPaneLeaf = this.app.workspace.splitActiveLeaf(
                    this.settings.newPaneSplitDirection || 'horizontal'
                );
                leafToUse = this.newPaneLeaf;
            }
        }
        await leafToUse.openFile(file);
        const editor = await utils.getEditor(this.app, leafToUse);
        if (editor && editorAction) await editorAction(editor);
    }

    /**
     * Open and go to the editor location represented by the marker
     * @param marker The FileMarker to open
     * @param useCtrlKeyBehavior If true will use the alternative behaviour, as set in the settings
     * @param highlight If true will highlight the line
     */
    async goToMarker(
        marker: FileMarker,
        useCtrlKeyBehavior: boolean,
        highlight: boolean
    ) {
        return this.goToFile(
            marker.file,
            useCtrlKeyBehavior,
            async (editor) => {
                await utils.goToEditorLocation(
                    editor,
                    marker.fileLocation,
                    highlight
                );
            }
        );
    }

    /**
     * Update the map markers with a list of markers not from the removed file plus the markers from the new file.
     * Run when a file is deleted, renamed or changed.
     * @param fileRemoved The old file path
     * @param fileAddedOrChanged The new file data
     */
    private async updateMarkersWithRelationToFile(
        fileRemoved: string,
        fileAddedOrChanged: TAbstractFile,
        skipMetadata: boolean
    ) {
        if (!this.display.map || !this.isOpen)
            // If the map has not been set up yet then do nothing
            return;
        let newMarkers: FileMarker[] = [];
        // Create an array of all file markers not in the removed file
        for (let [markerId, fileMarker] of this.display.markers) {
            if (fileMarker.file.path !== fileRemoved)
                newMarkers.push(fileMarker);
        }
        if (fileAddedOrChanged && fileAddedOrChanged instanceof TFile)
            // Add file markers from the added file
            await buildAndAppendFileMarkers(
                newMarkers,
                fileAddedOrChanged,
                this.settings,
                this.app
            );
        finalizeMarkers(newMarkers, this.settings);
        this.updateMapMarkers(newMarkers);
    }

    addSearchResultMarker(details: GeoSearchResult) {
        this.display.searchResult = leaflet.marker(details.location, {
            icon: getIconFromOptions(consts.SEARCH_RESULT_MARKER),
        });
        const marker = this.display.searchResult;
        marker.on('mouseover', (event: leaflet.LeafletMouseEvent) => {
            marker
                .bindPopup(details.name, {
                    closeButton: true,
                    className: 'marker-popup',
                })
                .openPopup();
        });
        marker.on('mouseout', (event: leaflet.LeafletMouseEvent) => {
            marker.closePopup();
        });
        marker.addTo(this.display.map);
        this.zoomToSearchResult(details.location);
    }

    zoomToSearchResult(location: leaflet.LatLng) {
        let currentState = this.leaf?.getViewState();
        if (currentState) {
            (currentState.state as MapState).mapCenter = location;
            (currentState.state as MapState).mapZoom =
                this.settings.zoomOnGoFromNote;
            this.leaf.setViewState(currentState);
        }
    }

    removeSearchResultMarker() {
        if (this.display.searchResult) {
            this.display.searchResult.removeFrom(this.display.map);
            this.display.searchResult = null;
        }
    }

    openSearch() {
        this.display.searchControls.openSearch(this.display.markers);
    }
}
Example #7
Source File: view.ts    From obsidian-fantasy-calendar with MIT License 4 votes vote down vote up
export default class FantasyCalendarView extends ItemView {
    dropdownEl: HTMLDivElement;
    helper: CalendarHelper;
    noCalendarEl: HTMLDivElement;
    updateMe: boolean = true;
    dayNumber: boolean;
    /* full =  false; */
    get root() {
        return this.leaf.getRoot();
    }
    get full() {
        return !("collapse" in this.root);
    }
    yearView: boolean = false;
    moons: boolean = true;
    calendar: Calendar;
    /* calendarDropdownEl: HTMLDivElement; */
    protected _app: CalendarUI;
    constructor(
        public plugin: FantasyCalendar,
        public leaf: WorkspaceLeaf,
        public options: { calendar?: Calendar; full?: boolean } = {}
    ) {
        super(leaf);
        this.containerEl.addClass("fantasy-calendar-view");
        this.contentEl.addClass("fantasy-calendar-view-content");
        this.registerEvent(
            this.plugin.app.workspace.on("fantasy-calendars-updated", () => {
                this.plugin.onSettingsLoad(() => this.updateCalendars());
            })
        );
        this.registerEvent(
            this.plugin.app.workspace.on("layout-change", () => {
                if (!this._app) return;
                this._app.$set({
                    fullView: this.full,
                    ...(this.full ? { dayView: false } : {})
                });
            })
        );
        /* window.view = this; */
    }
    updateCalendars() {
        if (!this.updateMe) {
            this.updateMe = true;
            return;
        }
        if (!this.plugin.data.calendars.length) {
            this._app?.$destroy();
            this.contentEl.empty();
            this.noCalendarEl = this.contentEl.createDiv("fantasy-no-calendar");
            this.noCalendarEl.createSpan({
                text: "No calendars created! Create a calendar to see it here."
            });
            return;
        }

        const calendar =
            this.plugin.data.calendars.find((c) => c.id == this.calendar?.id) ??
            this.plugin.defaultCalendar ??
            this.plugin.data.calendars[0];

        if (this.helper && this.helper.calendar.id == calendar.id) {
            this.update(calendar);
        } else {
            this.setCurrentCalendar(calendar);
        }
    }
    update(calendar: Calendar) {
        this.calendar = calendar;
        this.helper.update(this.calendar);

        this.registerCalendarInterval();

        if (!this._app) {
            this.build();
        } else {
            this._app.$set({ calendar: this.helper });
        }
    }

    registerCalendarInterval() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
        }

        if (this.calendar.static.incrementDay) {
            let current = new Date();

            if (!this.calendar.date) {
                this.calendar.date = current.valueOf();
            }

            const dif = daysBetween(new Date(this.calendar.date), current);

            if (dif >= 1) {
                for (let i = 0; i < dif; i++) {
                    this.helper.goToNextCurrentDay();
                }
                this.calendar.date = current.valueOf();
                this.saveCalendars();
            }
            this.interval = window.setInterval(() => {
                if (daysBetween(new Date(), current) >= 1) {
                    this.helper.goToNextCurrentDay();
                    this.helper.current;
                    current = new Date();
                    this.calendar.date = current.valueOf();
                    this.saveCalendars();
                }
            }, 60 * 1000);

            this.registerInterval(this.interval);
        }
    }

    saveCalendars() {
        this.updateMe = false;
        this.plugin.saveCalendar();
    }

    interval: number;

    setCurrentCalendar(calendar: Calendar) {
        this.noCalendarEl?.detach();

        this.calendar = calendar;

        this.moons = this.calendar.static.displayMoons;
        this.dayNumber = this.calendar.static.displayDayNumber;
        this.helper = new CalendarHelper(this.calendar, this.plugin);

        this.registerCalendarInterval();

        this.build();
    }
    createEventForDay(date: CurrentCalendarData) {
        const modal = new CreateEventModal(
            this.plugin,
            this.calendar,
            null,
            date
        );

        modal.onClose = () => {
            if (!modal.saved) return;
            this.calendar.events.push(modal.event);

            this.helper.addEvent(modal.event);

            this.saveCalendars();

            this._app.$set({
                calendar: this.helper
            });

            this.triggerHelperEvent("day-update");
        };

        modal.open();
    }

    async onOpen() {
        this.plugin.onSettingsLoad(() => this.updateCalendars());
    }
    build() {
        this.contentEl.empty();
        this._app = new CalendarUI({
            target: this.contentEl,
            props: {
                calendar: this.helper,
                fullView: this.full,
                yearView: this.yearView,
                moons: this.moons,
                displayWeeks: this.helper.displayWeeks,
                displayDayNumber: this.dayNumber
            }
        });
        this._app.$on("day-click", (event: CustomEvent<DayHelper>) => {
            const day = event.detail;

            if (day.events.length) return;
            this.createEventForDay(day.date);
        });

        this._app.$on("day-doubleclick", (event: CustomEvent<DayHelper>) => {
            const day = event.detail;
            if (!day.events.length) return;

            this.helper.viewing.day = day.number;
            this.helper.viewing.month = day.month.number;
            this.helper.viewing.year = day.month.year;

            this.yearView = false;

            this._app.$set({ yearView: false });
            this._app.$set({ dayView: true });
            this.triggerHelperEvent("day-update", false);
        });

        this._app.$on(
            "day-context-menu",
            (event: CustomEvent<{ day: DayHelper; evt: MouseEvent }>) => {
                const { day, evt } = event.detail;

                const menu = new Menu(this.app);

                menu.setNoIcon();

                if (!this.full) {
                    menu.addItem((item) => {
                        item.setTitle("Open Day").onClick(() => {
                            this.openDay({
                                day: day.number,
                                month: this.helper.displayed.month,
                                year: this.helper.displayed.year
                            });
                        });
                    });
                }
                menu.addItem((item) => {
                    item.setTitle("Set as Today").onClick(() => {
                        this.calendar.current = day.date;

                        this.helper.current.day = day.number;

                        this.triggerHelperEvent("day-update");

                        this.saveCalendars();
                    });
                });
                menu.addItem((item) =>
                    item.setTitle("New Event").onClick(() => {
                        this.createEventForDay(day.date);
                    })
                );
                menu.showAtMouseEvent(evt);
            }
        );

        this._app.$on("settings", (event: CustomEvent<MouseEvent>) => {
            const evt = event.detail;
            const menu = new Menu(this.app);

            menu.setNoIcon();
            menu.addItem((item) => {
                item.setTitle(
                    `${this.calendar.displayWeeks ? "Hide" : "Show"} Weeks`
                ).onClick(() => {
                    this.calendar.displayWeeks = !this.calendar.displayWeeks;
                    this.helper.update(this.calendar);
                    this._app.$set({
                        displayWeeks: this.calendar.displayWeeks
                    });
                    this.saveCalendars();
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    `Open ${this.yearView ? "Month" : "Year"}`
                ).onClick(() => {
                    this.yearView = !this.yearView;
                    this._app.$set({ yearView: this.yearView });
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    this.moons ? "Hide Moons" : "Display Moons"
                ).onClick(() => {
                    this.toggleMoons();
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    this.dayNumber ? "Hide Day Number" : "Display Day Number"
                ).onClick(() => {
                    this.dayNumber = !this.dayNumber;
                    this.calendar.static.displayDayNumber = this.dayNumber;
                    this._app.$set({ displayDayNumber: this.dayNumber });
                    this.saveCalendars();
                });
            });
            menu.addItem((item) => {
                item.setTitle("View Day");

                item.onClick(() => {
                    this.openDate();
                });
            });
            menu.addItem((item) => {
                item.setTitle("Switch Calendars");
                item.setDisabled(this.plugin.data.calendars.length <= 1);
                item.onClick(() => {
                    const modal = new SwitchModal(this.plugin, this.calendar);

                    modal.onClose = () => {
                        if (!modal.confirmed) return;

                        this.setCurrentCalendar(modal.calendar);
                    };
                    modal.open();
                });
            });

            menu.showAtMouseEvent(evt);
        });

        this._app.$on(
            "event-click",
            (evt: CustomEvent<{ event: Event; modifier: boolean }>) => {
                const { event, modifier } = evt.detail;
                if (event.note) {
                    let leaves: WorkspaceLeaf[] = [];
                    this.app.workspace.iterateAllLeaves((leaf) => {
                        if (!(leaf.view instanceof MarkdownView)) return;
                        if (leaf.view.file.basename === event.note) {
                            leaves.push(leaf);
                        }
                    });
                    if (leaves.length) {
                        this.app.workspace.setActiveLeaf(leaves[0]);
                    } else {
                        this.app.workspace.openLinkText(
                            event.note,
                            "",
                            this.full || modifier
                        );
                    }
                } else {
                    const modal = new ViewEventModal(event, this.plugin);
                    modal.open();
                }
            }
        );

        this._app.$on(
            "event-mouseover",
            (evt: CustomEvent<{ target: HTMLElement; event: Event }>) => {
                if (!this.plugin.data.eventPreview) return;
                const { target, event } = evt.detail;
                if (event.note) {
                    this.app.workspace.trigger(
                        "link-hover",
                        this, //hover popover, but don't need
                        target, //targetEl
                        event.note, //linkText
                        "" //source
                    );
                }
            }
        );

        this._app.$on(
            "event-context",
            (custom: CustomEvent<{ evt: MouseEvent; event: Event }>) => {
                const { evt, event } = custom.detail;

                const menu = new Menu(this.app);

                menu.setNoIcon();

                if (!event.note) {
                    menu.addItem((item) => {
                        item.setTitle("Create Note").onClick(async () => {
                            const path =
                                this.app.workspace.getActiveFile()?.path;
                            const newFilePath = path
                                ? this.app.fileManager.getNewFileParent(path)
                                      ?.parent ?? "/"
                                : "/";

                            const date = `${event.date.year}-${
                                event.date.month + 1
                            }-${event.date.day}`;

                            let end: string;
                            if (event.end) {
                                end = `${event.end.year}-${
                                    event.end.month + 1
                                }-${event.end.day}`;
                            }

                            const content = {
                                "fc-calendar": this.calendar.name,
                                "fc-date": date,
                                ...(event.end ? { "fc-end": end } : {}),
                                ...(event.category
                                    ? {
                                          "fc-category":
                                              this.calendar.categories.find(
                                                  (cat) =>
                                                      cat.id == event.category
                                              )?.name
                                      }
                                    : {}),
                                "fc-display-name": event.name
                            };
                            event.note = normalizePath(
                                `${newFilePath}/${event.name}.md`
                            );

                            let file = this.app.vault.getAbstractFileByPath(
                                event.note
                            );
                            if (!file) {
                                file = await this.app.vault.create(
                                    event.note,
                                    `---\n${stringifyYaml(content)}\n---`
                                );
                            }
                            this.saveCalendars();

                            if (file instanceof TFile) {
                                const fileViews =
                                    this.app.workspace.getLeavesOfType(
                                        "markdown"
                                    );
                                const existing = fileViews.find((l) => {
                                    l.view instanceof FileView &&
                                        l.view.file.path == event.note;
                                });
                                if (existing) {
                                    this.app.workspace.setActiveLeaf(existing);
                                } else {
                                    await this.app.workspace
                                        .getUnpinnedLeaf()
                                        .openFile(file, {
                                            active: true
                                        });
                                }
                            }
                        });
                    });
                }

                menu.addItem((item) => {
                    item.setTitle("Edit Event").onClick(() => {
                        const modal = new CreateEventModal(
                            this.plugin,
                            this.calendar,
                            event
                        );

                        modal.onClose = () => {
                            if (!modal.saved) return;

                            const existing = this.calendar.events.find(
                                (e) => e.id == event.id
                            );

                            this.calendar.events.splice(
                                this.calendar.events.indexOf(existing),
                                1,
                                modal.event
                            );

                            this.helper.refreshMonth(
                                modal.event.date.month,
                                modal.event.date.year
                            );
                            if (
                                modal.event.date.month != existing.date.month ||
                                modal.event.date.year != existing.date.year
                            ) {
                                this.helper.refreshMonth(
                                    existing.date.month,
                                    existing.date.year
                                );
                            }

                            this.saveCalendars();

                            this._app.$set({
                                calendar: this.helper
                            });

                            this.triggerHelperEvent("day-update");
                        };

                        modal.open();
                    });
                });

                menu.addItem((item) => {
                    item.setTitle("Delete Event").onClick(async () => {
                        if (
                            !this.plugin.data.exit.event &&
                            !(await confirmEventDeletion(this.plugin))
                        )
                            return;
                        const existing = this.calendar.events.find(
                            (e) => e.id == event.id
                        );

                        this.calendar.events.splice(
                            this.calendar.events.indexOf(existing),
                            1
                        );

                        this.helper.refreshMonth(
                            existing.date.month,
                            existing.date.year
                        );

                        this.saveCalendars();

                        this._app.$set({
                            calendar: this.helper
                        });

                        this.triggerHelperEvent("day-update");
                    });
                });

                menu.showAtMouseEvent(evt);
            }
        );

        this._app.$on("event", (e: CustomEvent<CurrentCalendarData>) => {
            const date = e.detail;
            this.createEventForDay(date);
        });

        this._app.$on("reset", () => {
            this.helper.reset();

            this.yearView = false;

            this._app.$set({ yearView: false });
            this._app.$set({ dayView: true });
            this.triggerHelperEvent("day-update", false);
        });
    }
    openDay(date: CurrentCalendarData) {
        this.helper.viewing.day = date.day;
        this.helper.viewing.month = date.month;
        this.helper.viewing.year = date.year;

        this.yearView = false;

        this._app.$set({ yearView: false });
        this._app.$set({ dayView: true });
        this.triggerHelperEvent("day-update", false);
    }
    openDate() {
        if (!this.helper) return;
        if (!this.calendar) return;
        const modal = new ChangeDateModal(this.plugin, this.calendar);
        modal.onClose = () => {
            if (!modal.confirmed) return;
            if (modal.setCurrent) {
                this.calendar.current = { ...modal.date };
                this.setCurrentCalendar(this.calendar);
            } else {
                this.helper.displayed = { ...modal.date };
                this.helper.update();
                this._app.$set({ calendar: this.helper });
            }

            this.saveCalendars();
        };

        modal.open();
    }
    toggleMoons() {
        this.moons = !this.moons;
        this._app.$set({ moons: this.moons });
    }

    async onClose() {}
    onResize() {
        this.triggerHelperEvent("view-resized", false);
    }
    getViewType() {
        return VIEW_TYPE;
    }
    getDisplayText() {
        return "Fantasy Calendar";
    }
    getIcon() {
        return VIEW_TYPE;
    }
    triggerHelperEvent(event: string, full: boolean = true) {
        if (!this.helper) return;
        this.helper.trigger(event);

        if (full) {
            this.updateMe = false;
            this.plugin.app.workspace.trigger("fantasy-calendars-updated");
        }
    }

    async onunload() {}
}
Example #8
Source File: view.ts    From obsidian-calendar-plugin with MIT License 4 votes vote down vote up
export default class CalendarView extends ItemView {
  private calendar: Calendar;
  private settings: ISettings;

  constructor(leaf: WorkspaceLeaf) {
    super(leaf);

    this.openOrCreateDailyNote = this.openOrCreateDailyNote.bind(this);
    this.openOrCreateWeeklyNote = this.openOrCreateWeeklyNote.bind(this);

    this.onNoteSettingsUpdate = this.onNoteSettingsUpdate.bind(this);
    this.onFileCreated = this.onFileCreated.bind(this);
    this.onFileDeleted = this.onFileDeleted.bind(this);
    this.onFileModified = this.onFileModified.bind(this);
    this.onFileOpen = this.onFileOpen.bind(this);

    this.onHoverDay = this.onHoverDay.bind(this);
    this.onHoverWeek = this.onHoverWeek.bind(this);

    this.onContextMenuDay = this.onContextMenuDay.bind(this);
    this.onContextMenuWeek = this.onContextMenuWeek.bind(this);

    this.registerEvent(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (<any>this.app.workspace).on(
        "periodic-notes:settings-updated",
        this.onNoteSettingsUpdate
      )
    );
    this.registerEvent(this.app.vault.on("create", this.onFileCreated));
    this.registerEvent(this.app.vault.on("delete", this.onFileDeleted));
    this.registerEvent(this.app.vault.on("modify", this.onFileModified));
    this.registerEvent(this.app.workspace.on("file-open", this.onFileOpen));

    this.settings = null;
    settings.subscribe((val) => {
      this.settings = val;

      // Refresh the calendar if settings change
      if (this.calendar) {
        this.calendar.tick();
      }
    });
  }

  getViewType(): string {
    return VIEW_TYPE_CALENDAR;
  }

  getDisplayText(): string {
    return "Calendar";
  }

  getIcon(): string {
    return "calendar-with-checkmark";
  }

  onClose(): Promise<void> {
    if (this.calendar) {
      this.calendar.$destroy();
    }
    return Promise.resolve();
  }

  async onOpen(): Promise<void> {
    // Integration point: external plugins can listen for `calendar:open`
    // to feed in additional sources.
    const sources = [
      customTagsSource,
      streakSource,
      wordCountSource,
      tasksSource,
    ];
    this.app.workspace.trigger(TRIGGER_ON_OPEN, sources);

    this.calendar = new Calendar({
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      target: (this as any).contentEl,
      props: {
        onClickDay: this.openOrCreateDailyNote,
        onClickWeek: this.openOrCreateWeeklyNote,
        onHoverDay: this.onHoverDay,
        onHoverWeek: this.onHoverWeek,
        onContextMenuDay: this.onContextMenuDay,
        onContextMenuWeek: this.onContextMenuWeek,
        sources,
      },
    });
  }

  onHoverDay(
    date: Moment,
    targetEl: EventTarget,
    isMetaPressed: boolean
  ): void {
    if (!isMetaPressed) {
      return;
    }
    const { format } = getDailyNoteSettings();
    const note = getDailyNote(date, get(dailyNotes));
    this.app.workspace.trigger(
      "link-hover",
      this,
      targetEl,
      date.format(format),
      note?.path
    );
  }

  onHoverWeek(
    date: Moment,
    targetEl: EventTarget,
    isMetaPressed: boolean
  ): void {
    if (!isMetaPressed) {
      return;
    }
    const note = getWeeklyNote(date, get(weeklyNotes));
    const { format } = getWeeklyNoteSettings();
    this.app.workspace.trigger(
      "link-hover",
      this,
      targetEl,
      date.format(format),
      note?.path
    );
  }

  private onContextMenuDay(date: Moment, event: MouseEvent): void {
    const note = getDailyNote(date, get(dailyNotes));
    if (!note) {
      // If no file exists for a given day, show nothing.
      return;
    }
    showFileMenu(this.app, note, {
      x: event.pageX,
      y: event.pageY,
    });
  }

  private onContextMenuWeek(date: Moment, event: MouseEvent): void {
    const note = getWeeklyNote(date, get(weeklyNotes));
    if (!note) {
      // If no file exists for a given day, show nothing.
      return;
    }
    showFileMenu(this.app, note, {
      x: event.pageX,
      y: event.pageY,
    });
  }

  private onNoteSettingsUpdate(): void {
    dailyNotes.reindex();
    weeklyNotes.reindex();
    this.updateActiveFile();
  }

  private async onFileDeleted(file: TFile): Promise<void> {
    if (getDateFromFile(file, "day")) {
      dailyNotes.reindex();
      this.updateActiveFile();
    }
    if (getDateFromFile(file, "week")) {
      weeklyNotes.reindex();
      this.updateActiveFile();
    }
  }

  private async onFileModified(file: TFile): Promise<void> {
    const date = getDateFromFile(file, "day") || getDateFromFile(file, "week");
    if (date && this.calendar) {
      this.calendar.tick();
    }
  }

  private onFileCreated(file: TFile): void {
    if (this.app.workspace.layoutReady && this.calendar) {
      if (getDateFromFile(file, "day")) {
        dailyNotes.reindex();
        this.calendar.tick();
      }
      if (getDateFromFile(file, "week")) {
        weeklyNotes.reindex();
        this.calendar.tick();
      }
    }
  }

  public onFileOpen(_file: TFile): void {
    if (this.app.workspace.layoutReady) {
      this.updateActiveFile();
    }
  }

  private updateActiveFile(): void {
    const { view } = this.app.workspace.activeLeaf;

    let file = null;
    if (view instanceof FileView) {
      file = view.file;
    }
    activeFile.setFile(file);

    if (this.calendar) {
      this.calendar.tick();
    }
  }

  public revealActiveNote(): void {
    const { moment } = window;
    const { activeLeaf } = this.app.workspace;

    if (activeLeaf.view instanceof FileView) {
      // Check to see if the active note is a daily-note
      let date = getDateFromFile(activeLeaf.view.file, "day");
      if (date) {
        this.calendar.$set({ displayedMonth: date });
        return;
      }

      // Check to see if the active note is a weekly-note
      const { format } = getWeeklyNoteSettings();
      date = moment(activeLeaf.view.file.basename, format, true);
      if (date.isValid()) {
        this.calendar.$set({ displayedMonth: date });
        return;
      }
    }
  }

  async openOrCreateWeeklyNote(
    date: Moment,
    inNewSplit: boolean
  ): Promise<void> {
    const { workspace } = this.app;

    const startOfWeek = date.clone().startOf("week");

    const existingFile = getWeeklyNote(date, get(weeklyNotes));

    if (!existingFile) {
      // File doesn't exist
      tryToCreateWeeklyNote(startOfWeek, inNewSplit, this.settings, (file) => {
        activeFile.setFile(file);
      });
      return;
    }

    const leaf = inNewSplit
      ? workspace.splitActiveLeaf()
      : workspace.getUnpinnedLeaf();
    await leaf.openFile(existingFile);

    activeFile.setFile(existingFile);
    workspace.setActiveLeaf(leaf, true, true)
  }

  async openOrCreateDailyNote(
    date: Moment,
    inNewSplit: boolean
  ): Promise<void> {
    const { workspace } = this.app;
    const existingFile = getDailyNote(date, get(dailyNotes));
    if (!existingFile) {
      // File doesn't exist
      tryToCreateDailyNote(
        date,
        inNewSplit,
        this.settings,
        (dailyNote: TFile) => {
          activeFile.setFile(dailyNote);
        }
      );
      return;
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const mode = (this.app.vault as any).getConfig("defaultViewMode");
    const leaf = inNewSplit
      ? workspace.splitActiveLeaf()
      : workspace.getUnpinnedLeaf();
    await leaf.openFile(existingFile, { active : true, mode });

    activeFile.setFile(existingFile);
  }
}
Example #9
Source File: sidebar.ts    From obsidian-spaced-repetition with MIT License 4 votes vote down vote up
export class ReviewQueueListView extends ItemView {
    private plugin: SRPlugin;

    constructor(leaf: WorkspaceLeaf, plugin: SRPlugin) {
        super(leaf);

        this.plugin = plugin;
        this.registerEvent(this.app.workspace.on("file-open", () => this.redraw()));
        this.registerEvent(this.app.vault.on("rename", () => this.redraw()));
    }

    public getViewType(): string {
        return REVIEW_QUEUE_VIEW_TYPE;
    }

    public getDisplayText(): string {
        return t("NOTES_REVIEW_QUEUE");
    }

    public getIcon(): string {
        return "SpacedRepIcon";
    }

    public onHeaderMenu(menu: Menu): void {
        menu.addItem((item) => {
            item.setTitle(t("CLOSE"))
                .setIcon("cross")
                .onClick(() => {
                    this.app.workspace.detachLeavesOfType(REVIEW_QUEUE_VIEW_TYPE);
                });
        });
    }

    public redraw(): void {
        const activeFile: TFile | null = this.app.workspace.getActiveFile();

        const rootEl: HTMLElement = createDiv("nav-folder mod-root");
        const childrenEl: HTMLElement = rootEl.createDiv("nav-folder-children");

        for (const deckKey in this.plugin.reviewDecks) {
            const deck: ReviewDeck = this.plugin.reviewDecks[deckKey];

            const deckCollapsed = !deck.activeFolders.has(deck.deckName);

            const deckFolderEl: HTMLElement = this.createRightPaneFolder(
                childrenEl,
                deckKey,
                deckCollapsed,
                false,
                deck
            ).getElementsByClassName("nav-folder-children")[0] as HTMLElement;

            if (deck.newNotes.length > 0) {
                const newNotesFolderEl: HTMLElement = this.createRightPaneFolder(
                    deckFolderEl,
                    t("NEW"),
                    !deck.activeFolders.has(t("NEW")),
                    deckCollapsed,
                    deck
                );

                for (const newFile of deck.newNotes) {
                    const fileIsOpen = activeFile && newFile.path === activeFile.path;
                    if (fileIsOpen) {
                        deck.activeFolders.add(deck.deckName);
                        deck.activeFolders.add(t("NEW"));
                        this.changeFolderIconToExpanded(newNotesFolderEl);
                        this.changeFolderIconToExpanded(deckFolderEl);
                    }
                    this.createRightPaneFile(
                        newNotesFolderEl,
                        newFile,
                        fileIsOpen,
                        !deck.activeFolders.has(t("NEW")),
                        deck,
                        this.plugin
                    );
                }
            }

            if (deck.scheduledNotes.length > 0) {
                const now: number = Date.now();
                let currUnix = -1;
                let schedFolderEl: HTMLElement | null = null,
                    folderTitle = "";
                const maxDaysToRender: number = this.plugin.data.settings.maxNDaysNotesReviewQueue;

                for (const sNote of deck.scheduledNotes) {
                    if (sNote.dueUnix != currUnix) {
                        const nDays: number = Math.ceil((sNote.dueUnix - now) / (24 * 3600 * 1000));

                        if (nDays > maxDaysToRender) {
                            break;
                        }

                        if (nDays === -1) {
                            folderTitle = t("YESTERDAY");
                        } else if (nDays === 0) {
                            folderTitle = t("TODAY");
                        } else if (nDays === 1) {
                            folderTitle = t("TOMORROW");
                        } else {
                            folderTitle = new Date(sNote.dueUnix).toDateString();
                        }

                        schedFolderEl = this.createRightPaneFolder(
                            deckFolderEl,
                            folderTitle,
                            !deck.activeFolders.has(folderTitle),
                            deckCollapsed,
                            deck
                        );
                        currUnix = sNote.dueUnix;
                    }

                    const fileIsOpen = activeFile && sNote.note.path === activeFile.path;
                    if (fileIsOpen) {
                        deck.activeFolders.add(deck.deckName);
                        deck.activeFolders.add(folderTitle);
                        this.changeFolderIconToExpanded(schedFolderEl);
                        this.changeFolderIconToExpanded(deckFolderEl);
                    }

                    this.createRightPaneFile(
                        schedFolderEl,
                        sNote.note,
                        fileIsOpen,
                        !deck.activeFolders.has(folderTitle),
                        deck,
                        this.plugin
                    );
                }
            }
        }

        const contentEl: Element = this.containerEl.children[1];
        contentEl.empty();
        contentEl.appendChild(rootEl);
    }

    private createRightPaneFolder(
        parentEl: HTMLElement,
        folderTitle: string,
        collapsed: boolean,
        hidden: boolean,
        deck: ReviewDeck
    ): HTMLElement {
        const folderEl: HTMLDivElement = parentEl.createDiv("nav-folder");
        const folderTitleEl: HTMLDivElement = folderEl.createDiv("nav-folder-title");
        const childrenEl: HTMLDivElement = folderEl.createDiv("nav-folder-children");
        const collapseIconEl: HTMLDivElement = folderTitleEl.createDiv(
            "nav-folder-collapse-indicator collapse-icon"
        );

        collapseIconEl.innerHTML = COLLAPSE_ICON;
        if (collapsed) {
            (collapseIconEl.childNodes[0] as HTMLElement).style.transform = "rotate(-90deg)";
        }

        folderTitleEl.createDiv("nav-folder-title-content").setText(folderTitle);

        if (hidden) {
            folderEl.style.display = "none";
        }

        folderTitleEl.onClickEvent(() => {
            for (const child of childrenEl.childNodes as NodeListOf<HTMLElement>) {
                if (child.style.display === "block" || child.style.display === "") {
                    child.style.display = "none";
                    (collapseIconEl.childNodes[0] as HTMLElement).style.transform =
                        "rotate(-90deg)";
                    deck.activeFolders.delete(folderTitle);
                } else {
                    child.style.display = "block";
                    (collapseIconEl.childNodes[0] as HTMLElement).style.transform = "";
                    deck.activeFolders.add(folderTitle);
                }
            }
        });

        return folderEl;
    }

    private createRightPaneFile(
        folderEl: HTMLElement,
        file: TFile,
        fileElActive: boolean,
        hidden: boolean,
        deck: ReviewDeck,
        plugin: SRPlugin
    ): void {
        const navFileEl: HTMLElement = folderEl
            .getElementsByClassName("nav-folder-children")[0]
            .createDiv("nav-file");
        if (hidden) {
            navFileEl.style.display = "none";
        }

        const navFileTitle: HTMLElement = navFileEl.createDiv("nav-file-title");
        if (fileElActive) {
            navFileTitle.addClass("is-active");
        }

        navFileTitle.createDiv("nav-file-title-content").setText(file.basename);
        navFileTitle.addEventListener(
            "click",
            (event: MouseEvent) => {
                event.preventDefault();
                plugin.lastSelectedReviewDeck = deck.deckName;
                this.app.workspace.activeLeaf.openFile(file);
                return false;
            },
            false
        );

        navFileTitle.addEventListener(
            "contextmenu",
            (event: MouseEvent) => {
                event.preventDefault();
                const fileMenu: Menu = new Menu(this.app);
                this.app.workspace.trigger("file-menu", fileMenu, file, "my-context-menu", null);
                fileMenu.showAtPosition({
                    x: event.pageX,
                    y: event.pageY,
                });
                return false;
            },
            false
        );
    }

    private changeFolderIconToExpanded(folderEl: HTMLElement): void {
        const collapseIconEl = folderEl.find("div.nav-folder-collapse-indicator");
        (collapseIconEl.childNodes[0] as HTMLElement).style.transform = "";
    }
}
Example #10
Source File: view.ts    From obsidian-initiative-tracker with GNU General Public License v3.0 4 votes vote down vote up
export default class TrackerView extends ItemView {
    async saveEncounter(name: string) {
        if (!name) {
            new Notice("An encounter must have a name to be saved.");
            return;
        }
        this.plugin.data.encounters[name] = {
            creatures: [...this.ordered.map((c) => c.toJSON())],
            state: this.state,
            name,
            round: this.round
        };
        await this.plugin.saveSettings();
    }
    async loadEncounter(name: string) {
        const state = this.plugin.data.encounters[name];
        if (!state) {
            new Notice("There was an issue loading the encounter.");
            return;
        }
        this.newEncounterFromState(state);
    }
    toggleCondensed() {
        this.condense = !this.condense;
        this.setAppState({ creatures: this.ordered });
    }
    setCondensed(bool: boolean) {
        this.condense = bool;
        this.setAppState({ creatures: this.ordered });
    }
    async openCombatant(creature: Creature) {
        const view = this.plugin.combatant;
        if (!view) {
            const leaf = this.app.workspace.getRightLeaf(true);
            await leaf.setViewState({
                type: CREATURE_TRACKER_VIEW
            });
        }
        this.ordered.forEach((c) => (c.viewing = false));
        creature.viewing = true;
        this.setAppState({ creatures: this.ordered });
        const ref = this.app.workspace.on(
            "initiative-tracker:stop-viewing",
            () => {
                creature.viewing = false;
                this.setAppState({ creatures: this.ordered });
                this.app.workspace.offref(ref);
            }
        );
        this.registerEvent(ref);

        this.plugin.combatant.render(creature);
    }
    protected creatures: Creature[] = [];

    public state: boolean = false;

    public name: string;

    public condense = this.plugin.data.condense;

    public round: number = 1;

    private _app: App;
    private _rendered: boolean = false;

    get pcs() {
        return this.players;
    }
    get npcs() {
        return this.creatures.filter((c) => !c.player);
    }
    party: Party = this.plugin.defaultParty;
    async switchParty(party: string) {
        if (!this.plugin.data.parties.find((p) => p.name == party)) return;
        this.party = this.plugin.data.parties.find((p) => p.name == party);
        this.setAppState({ party: this.party.name });
        this.creatures = this.creatures.filter((p) => !p.player);
        for (const player of this.players) {
            player.initiative = await this.getInitiativeValue(player.modifier);
            this._addCreature(player);
        }
    }
    playerNames: string[] = [];
    get players() {
        if (this.party) {
            let players = this.party.players;
            if (players) {
                return Array.from(this.plugin.playerCreatures.values()).filter(
                    (p) => players.includes(p.name)
                );
            }
        }
        return Array.from(this.plugin.playerCreatures.values());
    }

    updatePlayers() {
        this.trigger("initiative-tracker:players-updated", this.pcs);
        this.setAppState({
            creatures: this.ordered
        });
    }

    updateState() {
        this.setAppState(this.appState);
    }

    constructor(public leaf: WorkspaceLeaf, public plugin: InitiativeTracker) {
        super(leaf);
        if (this.plugin.data.state?.creatures?.length) {
            this.newEncounterFromState(this.plugin.data.state);
        } else {
            this.newEncounter();
        }
    }
    newEncounterFromState(initiativeState: InitiativeViewState) {
        if (!initiativeState || !initiativeState?.creatures?.length) {
            this.newEncounter();
        }
        const { creatures, state, name, round = 1 } = initiativeState;
        this.setCreatures([...creatures.map((c) => Creature.fromJSON(c))]);

        this.name = name;
        this.round = round;
        this.state = state;
        this.trigger("initiative-tracker:new-encounter", this.appState);

        this.setAppState({
            creatures: this.ordered,
            state: this.state,
            round: this.round,
            name: this.name
        });
    }
    private _addCreature(creature: Creature) {
        this.addCreatures([creature], false);
        /* this.creatures.push(creature);

        this.setAppState({
            creatures: this.ordered
        }); */
    }
    get condensed() {
        if (this.condense) {
            this.creatures.forEach((creature, _, arr) => {
                const equiv = arr.filter((c) => equivalent(c, creature));
                equiv.forEach((eq) => {
                    eq.initiative = Math.max(...equiv.map((i) => i.initiative));
                });
            });
        }
        return this.creatures;
    }
    get ordered() {
        const sort = [...this.condensed];
        sort.sort((a, b) => {
            return b.initiative - a.initiative;
        });
        return sort;
    }

    get enabled() {
        return this.ordered.filter((c) => c.enabled);
    }

    addCreatures(creatures: Creature[], trigger = true) {
        /* for (let creature of creatures) {
            this.creatures.push(creature);
        } */

        this.setCreatures([...(this.creatures ?? []), ...(creatures ?? [])]);

        if (trigger)
            this.trigger("initiative-tracker:creatures-added", creatures);

        this.setAppState({
            creatures: this.ordered
        });
    }

    removeCreature(...creatures: Creature[]) {
        if (creatures.some((c) => c.active)) {
            const active = this.creatures.find((c) => c.active);
            this.goToNext();
            this.setCreatures(this.creatures.filter((c) => c != active));
            this.removeCreature(...creatures.filter((c) => c != active));
            return;
        }
        this.setCreatures(this.creatures.filter((c) => !creatures.includes(c)));
        this.trigger("initiative-tracker:creatures-removed", creatures);
        this.setAppState({
            creatures: this.ordered
        });
    }

    setCreatures(creatures: Creature[]) {
        this.creatures = creatures;

        for (let i = 0; i < this.creatures.length; i++) {
            const creature = this.creatures[i];
            if (
                creature.player ||
                this.creatures.filter((c) => c.name == creature.name).length ==
                    1
            ) {
                continue;
            }
            if (creature.number > 0) continue;
            const prior = this.creatures
                .slice(0, i)
                .filter((c) => c.name == creature.name)
                .map((c) => c.number);
            creature.number = prior?.length ? Math.max(...prior) + 1 : 1;
        }
    }

    async newEncounter({
        name,
        party = this.party?.name,
        players = [...this.plugin.data.players.map((p) => p.name)],
        creatures = [],
        roll = true,
        xp = null
    }: {
        party?: string;
        name?: string;
        players?: string[];
        creatures?: Creature[];
        roll?: boolean;
        xp?: number;
    } = {}) {
        this.creatures = [];
        const playerNames: Set<string> = new Set(players ?? []);
        if (party && party != this.party?.name) {
            this.party = this.plugin.data.parties.find((p) => p.name == party);
            for (const player of this.players) {
                playerNames.add(player.name);
            }
        }
        for (const player of playerNames) {
            if (!this.plugin.playerCreatures.has(player)) continue;
            this.creatures.push(this.plugin.playerCreatures.get(player));
        }

        if (creatures) this.setCreatures([...this.creatures, ...creatures]);

        this.name = name;
        this.round = 1;
        this.setAppState({
            party: this.party?.name,
            name: this.name,
            round: this.round,
            xp
        });

        for (let creature of this.creatures) {
            creature.enabled = true;
        }

        this.trigger("initiative-tracker:new-encounter", this.appState);

        if (roll) await this.rollInitiatives();
        else {
            this.setAppState({
                creatures: this.ordered
            });
        }
    }

    resetEncounter() {
        for (let creature of this.ordered) {
            creature.hp = creature.max;
            this.setCreatureState(creature, true);
            const statuses = Array.from(creature.status);
            statuses.forEach((status) => {
                this.removeStatus(creature, status);
            });
            creature.active = false;
        }

        if (this.ordered.length) this.ordered[0].active = true;

        this.setAppState({
            creatures: this.ordered
        });
    }
    setMapState(v: boolean) {
        this.setAppState({
            map: v
        });
    }
    async getInitiativeValue(modifier: number = 0): Promise<number> {
        return await this.plugin.getInitiativeValue(modifier);
    }

    async rollInitiatives() {
        for (let creature of this.creatures) {
            creature.initiative = await this.getInitiativeValue(
                creature.modifier
            );
            creature.active = false;
        }

        if (this.ordered.length) this.ordered[0].active = true;

        this.setAppState({
            creatures: this.ordered
        });
    }
    get appState() {
        return {
            state: this.state,
            pcs: this.pcs,
            npcs: this.npcs,
            creatures: this.ordered
        };
    }
    goToNext(active = this.ordered.findIndex((c) => c.active)) {
        /* const active = this.ordered.findIndex((c) => c.active); */
        if (active == -1) return;
        const sliced = [
            ...this.ordered.slice(active + 1),
            ...this.ordered.slice(0, active)
        ];
        const next = sliced.find((c) => c.enabled);
        if (this.ordered[active]) this.ordered[active].active = false;
        if (!next) return;
        if (active > this.ordered.indexOf(next)) this.round++;
        next.active = true;

        this.trigger("initiative-tracker:active-change", next);

        this.setAppState({
            creatures: this.ordered,
            round: this.round
        });
    }
    goToPrevious(active = this.ordered.findIndex((c) => c.active)) {
        /* const active = this.ordered.findIndex((c) => c.active); */
        if (active == -1) return;

        const previous = [...this.ordered].slice(0, active).reverse();
        const after = [...this.ordered].slice(active + 1).reverse();
        const creature = [...previous, ...after].find((c) => c.enabled);
        if (this.ordered[active]) this.ordered[active].active = false;
        if (!creature) return;
        if (active < this.ordered.indexOf(creature))
            this.round = Math.max(1, this.round - 1);
        creature.active = true;
        this.trigger("initiative-tracker:active-change", creature);
        this.setAppState({
            creatures: this.ordered,
            round: this.round
        });
    }
    toggleState() {
        this.state = !this.state;
        this.creatures.forEach((c) => (c.active = false));
        if (this.state) {
            const active = this.ordered.find((c) => c.enabled);
            if (active) {
                active.active = true;
                this.trigger("initiative-tracker:active-change", active);
            }
        } else {
            this.trigger("initiative-tracker:active-change", null);
        }

        this.setAppState({
            state: this.state
        });
    }
    addStatus(creature: Creature, tag: Condition) {
        creature.status.add(tag);

        this.trigger("initiative-tracker:creature-updated", creature);

        this.setAppState({
            creatures: this.ordered
        });
    }
    removeStatus(creature: Creature, tag: Condition) {
        creature.status.delete(tag);

        this.trigger("initiative-tracker:creature-updated", creature);

        this.setAppState({
            creatures: this.ordered
        });
    }
    updateCreature(
        creature: Creature,
        {
            hp,
            max,
            ac,
            initiative,
            name,
            marker
        }: {
            hp?: number;
            ac?: number;
            initiative?: number;
            name?: string;
            marker?: string;
            max?: number;
        }
    ) {
        if (initiative) {
            creature.initiative = Number(initiative);
        }
        if (name) {
            creature.name = name;
            creature.number = 0;
        }
        if (hp) {
            creature.hp += Number(hp);
        }
        if (max) {
            if (creature.hp == creature.max) {
                creature.hp = Number(max);
            }
            creature.max = Number(max);
        }
        if (ac) {
            creature.ac = ac;
        }
        if (marker) {
            creature.marker = marker;
        }
        this.trigger("initiative-tracker:creature-updated", creature);

        this.setAppState({
            creatures: this.ordered
        });
    }
    async copyInitiativeOrder() {
        const contents = this.ordered
            .map((creature) => `${creature.initiative} ${creature.name}`)
            .join("\n");
        await navigator.clipboard.writeText(contents);
    }
    setCreatureState(creature: Creature, enabled: boolean) {
        if (enabled) {
            this._enableCreature(creature);
        } else {
            this._disableCreature(creature);
        }

        this.trigger("initiative-tracker:creature-updated", creature);

        this.setAppState({
            creatures: this.ordered
        });
    }
    private _enableCreature(creature: Creature) {
        creature.enabled = true;
        if (this.enabled.length == 1) {
            creature.active = true;
        }
    }
    private _disableCreature(creature: Creature) {
        if (creature.active) {
            this.goToNext();
        }
        creature.enabled = false;
    }

    setAppState(state: Partial<App["$$prop_def"]>) {
        if (this._app && this._rendered) {
            this.plugin.app.workspace.trigger(
                "initiative-tracker:state-change",
                this.appState
            );
            this._app.$set(state);
        }

        this.plugin.data.state = this.toState();
        this.trigger("initiative-tracker:should-save");
    }
    async onOpen() {
        this._app = new App({
            target: this.contentEl,
            props: {
                party: this.party?.name,
                creatures: this.ordered,
                state: this.state,
                xp: null,
                view: this,
                plugin: this.plugin,
                round: this.round
            }
        });
        this._rendered = true;
    }
    async onClose() {
        this._app.$destroy();
        this._rendered = false;
        this.trigger("initiative-tracker:closed");
    }
    getViewType() {
        return INTIATIVE_TRACKER_VIEW;
    }
    getDisplayText() {
        return "Initiative Tracker";
    }
    getIcon() {
        return BASE;
    }
    openInitiativeView() {
        this.plugin.leaflet.openInitiativeView(this.pcs, this.npcs);
    }

    trigger(...args: TrackerEvents) {
        const [name, ...data] = args;
        this.app.workspace.trigger(name, ...data);
    }
    toState() {
        if (!this.state) return null;
        return {
            creatures: [...this.ordered.map((c) => c.toJSON())],
            state: this.state,
            name: this.name,
            round: this.round
        };
    }
    async onunload() {
        this.plugin.data.state = this.toState();
        await this.plugin.saveSettings();
    }
    registerEvents() {
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:add-creature-here",
                async (latlng: L.LatLng) => {
                    this.app.workspace.revealLeaf(this.leaf);
                    let addNewAsync = this._app.$on("add-new-async", (evt) => {
                        const creature = evt.detail;
                        this._addCreature(creature);

                        this.trigger(
                            "initiative-tracker:creature-added-at-location",
                            creature,
                            latlng
                        );
                        addNewAsync();
                        cancel();
                    });
                    let cancel = this._app.$on("cancel-add-new-async", () => {
                        addNewAsync();
                        cancel();
                    });
                    this._app.$set({ addNewAsync: true });
                }
            )
        );
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:creature-updated-in-settings",
                (creature: Creature) => {
                    const existing = this.creatures.find((c) => c == creature);

                    if (existing) {
                        this.updateCreature(existing, creature);
                    }
                }
            )
        );
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:remove",
                (creature: Creature) => {
                    const existing = this.creatures.find(
                        (c) => c.id == creature.id
                    );

                    if (existing) {
                        this.removeCreature(existing);
                    }
                }
            )
        );
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:enable-disable",
                (creature: Creature, enable: boolean) => {
                    const existing = this.creatures.find(
                        (c) => c.id == creature.id
                    );

                    if (existing) {
                        this.setCreatureState(existing, enable);
                    }
                }
            )
        );
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:apply-damage",
                (creature: Creature) => {
                    const existing = this.creatures.find(
                        (c) => c.id == creature.id
                    );

                    if (existing) {
                        this.setAppState({
                            updatingHP: existing
                        });
                    }
                }
            )
        );
        this.registerEvent(
            this.app.workspace.on(
                "initiative-tracker:add-status",
                (creature: Creature) => {
                    const existing = this.creatures.find(
                        (c) => c.id == creature.id
                    );

                    if (existing) {
                        this.setAppState({
                            updatingStatus: existing
                        });
                    }
                }
            )
        );
    }
}