obsidian-scraps/main.ts
2024-10-28 11:03:02 -04:00

343 lines
8.5 KiB
TypeScript

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<CommandId, CommandDef> = {
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<void>} A promise that resolves when the directory
* has been ensured.
*/
async ensureScrapDir(): Promise<void> {
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<void>} A promise that resolves when the scrap file is created and opened.
*/
async createNewMarkdownScrap(): Promise<void> {
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<void>} A promise that resolves when the file has been converted to a scrap.
*/
async moveCurrentFileToScraps(): Promise<void> {
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<void>} A promise that resolves when the file has been renamed.
*/
async renameCurrentFileAsScrap(): Promise<void> {
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<void>} A promise that resolves when the file has been copied and opened.
*/
async copyCurrentFileToScraps(): Promise<void> {
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 <moment format>}, {A}, {N}, {MW}");
}
}