import { App, Plugin, PluginSettingTab, Setting, Notice } from "obsidian"; import { format } from "./lib/format.js"; import { mkdirp } from "./lib/util.js"; import { ICON } from "./lib/resource/icons.js"; type NewScrapBehaviorOption = "default" | "tab" | "split" | "window"; export interface ScrapsPluginSettings { scrapsRootDir: string; scrapsPathFormat: string; scrapsFileName: string; newScrapBehavior: NewScrapBehaviorOption; } const DEFAULT_SETTINGS: ScrapsPluginSettings = { scrapsRootDir: "_Scraps", scrapsPathFormat: "{time YYYY} Week {time WW MMM}", scrapsFileName: "{time DDDD} {A} {N}", newScrapBehavior: "default", }; type CommandId = "New" | "Move" | "Copy" | "Rename"; interface CommandDef { id: string; icon: string; name: string; } const COMMANDS: Record = { New: { id: "scraps-new", icon: ICON.New, name: "Scraps: Create new Scrap", }, Move: { id: "scraps-move", icon: ICON.Move, name: "Scraps: Move current file to Scraps", }, Copy: { id: "scraps-copy", icon: ICON.Copy, name: "Scraps: Copy current file to Scraps", }, Rename: { id: "scraps-rename", icon: ICON.Rename, name: "Scraps: Rename current file as Scrap", }, }; export default class ScrapsPlugin extends Plugin { settings: ScrapsPluginSettings; /** * Ensures that the scrap directory exists by creating it if it does not * already exist. * * @returns {Promise} A promise that resolves when the directory * has been ensured. */ async ensureScrapDir(): Promise { await mkdirp(this.app.vault, this.scrapsDirectory); } /** * The current scrap directory path based on the configured root * directory and path format. * * @returns {string} The full path to the current scrap directory. */ get scrapsDirectory(): string { return `${this.settings.scrapsRootDir}/${format( this.settings.scrapsPathFormat )}`; } /** * A formatted scrap file name based on the current settings. * * @note This will generate a new name each time it is accessed. * @returns {string} The formatted scrap file name. */ get nextScrapFileName(): string { return format(this.settings.scrapsFileName); } get newScrapLeafType(): false | "tab" | "split" | "window" { switch (this.settings.newScrapBehavior) { case "tab": return "tab"; case "split": return "split"; case "window": return "window"; default: return false; } } /** * Creates a new scrap file in the designated scrap directory. * * @returns {Promise} A promise that resolves when the scrap file is created and opened. */ async createNewMarkdownScrap(): Promise { await this.ensureScrapDir(); const newScrap = await this.app.vault.create( `${this.scrapsDirectory}/${this.nextScrapFileName}.md`, "" ); await this.app.workspace .getLeaf(this.newScrapLeafType) .openFile(newScrap); } /** * Moves the currently active file to the Scraps directory. * * @returns {Promise} A promise that resolves when the file has been converted to a scrap. */ async moveCurrentFileToScraps(): Promise { const currentFile = this.app.workspace.getActiveFile(); if (currentFile === null) { new Notice("No file is currently open"); return; } await this.ensureScrapDir(); const renamePath = `${this.scrapsDirectory}/${currentFile.name}`; await this.app.fileManager.renameFile(currentFile, renamePath); } /** * Renames the currently active file to use a generated Scrap title * * @returns {Promise} A promise that resolves when the file has been renamed. */ async renameCurrentFileAsScrap(): Promise { const currentFile = this.app.workspace.getActiveFile(); if (currentFile === null) { new Notice("No file is currently open"); return; } await this.ensureScrapDir(); const renamePath = `${this.scrapsDirectory}/${this.nextScrapFileName}.${currentFile.extension}`; await this.app.fileManager.renameFile(currentFile, renamePath); } /** * Copies the currently active file to a designated "scrap" directory and opens the copied file. * If no file is currently open, a notice is displayed to the user. * * @returns {Promise} A promise that resolves when the file has been copied and opened. */ async copyCurrentFileToScraps(): Promise { const currentFile = this.app.workspace.getActiveFile(); if (currentFile === null) { new Notice("No file is currently open"); return; } await this.ensureScrapDir(); const newScrap = await this.app.vault.copy( currentFile, `${this.scrapsDirectory}/${currentFile.name}` ); await this.app.workspace .getLeaf(this.newScrapLeafType) .openFile(newScrap); } async onload() { await this.loadSettings(); this.addSettingTab(new ScrapsSettingTab(this.app, this)); /////////////////////////////// // Command: Create New Scrap // /////////////////////////////// this.addCommand({ ...COMMANDS.New, callback: async () => { this.createNewMarkdownScrap(); }, }); this.addRibbonIcon(COMMANDS.New.icon, COMMANDS.New.name, async () => { this.createNewMarkdownScrap(); }); ///////////////////////////// // Command: Move to Scraps // ///////////////////////////// this.addCommand({ ...COMMANDS.Move, editorCallback: async () => this.moveCurrentFileToScraps(), }); this.addRibbonIcon(COMMANDS.Move.icon, COMMANDS.Move.name, async () => { this.moveCurrentFileToScraps(); }); ///////////////////////////// // Command: Copy to Scraps // ///////////////////////////// this.addCommand({ ...COMMANDS.Copy, editorCallback: () => this.copyCurrentFileToScraps(), }); this.addRibbonIcon(COMMANDS.Copy.icon, COMMANDS.Copy.name, async () => { this.copyCurrentFileToScraps(); }); /////////////////////////////// // Command: Rename to Scraps // /////////////////////////////// this.addCommand({ ...COMMANDS.Rename, editorCallback: () => this.copyCurrentFileToScraps(), }); this.addRibbonIcon( COMMANDS.Rename.icon, COMMANDS.Rename.name, async () => { this.renameCurrentFileAsScrap(); } ); } onunload() {} async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, await this.loadData() ); } async saveSettings() { await this.saveData(this.settings); } } class ScrapsSettingTab extends PluginSettingTab { plugin: ScrapsPlugin; constructor(app: App, plugin: ScrapsPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); new Setting(containerEl) .setName("Scraps Root Directory") .setDesc("The directory where all your scraps will be stored") .addText((text) => text .setPlaceholder("Enter directory") .setValue(this.plugin.settings.scrapsRootDir) .onChange(async (value) => { this.plugin.settings.scrapsRootDir = value; await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName("New Scrap Behavior") .setDesc("The behavior when creating a new scrap") .addDropdown((dropdown) => dropdown .addOptions({ default: "Default", tab: "Open in new tab", split: "Split the current pane", window: "Open in new window", }) .setValue(this.plugin.settings.newScrapBehavior) .onChange(async (value) => { this.plugin.settings.newScrapBehavior = value as NewScrapBehaviorOption; await this.plugin.saveSettings(); }) ); const pathFormatSetting = new Setting(containerEl) .setName("Scraps Path Format") .setDesc( `Preview: ${format(this.plugin.settings.scrapsPathFormat)}` ) .addText((text) => text .setPlaceholder("Enter format") .setValue(this.plugin.settings.scrapsPathFormat) .onChange(async (value) => { this.plugin.settings.scrapsPathFormat = value; pathFormatSetting.setDesc(`Preview: ${format(value)}`); await this.plugin.saveSettings(); }) ); const fileFormatSetting = new Setting(containerEl) .setName("Scraps File Name Format") .setDesc( `Preview: ${ format(this.plugin.settings.scrapsFileName) + ".md" }` ) .addText((text) => text .setPlaceholder("Enter format") .setValue(this.plugin.settings.scrapsFileName) .onChange(async (value) => { this.plugin.settings.scrapsFileName = value; fileFormatSetting.setDesc( `Preview: ${format(value) + ".md"}` ); await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName("Formatting Help") .setDesc("Tokens: {time }, {A}, {N}, {MW}"); } }