Merge pull request #3 from EndarValuk/feature/SORT-FILES-BY-NAME

Feature/OverwriteExistingFileDialog
This commit is contained in:
Anto Keinänen 2023-05-10 12:28:05 +03:00 committed by GitHub
commit 4dcb1cb6d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 340 additions and 74 deletions

23
CHANGELOG.md Normal file
View file

@ -0,0 +1,23 @@
## obsidian-advanced-merger 1.3.0
### Additions and Changes
- Added "Overwrite existing file" dialog
## obsidian-advanced-merger 1.2.0
### Additions and Changes
- Translations support added (starter: English, Ukranian, Finnish, French, Russian, German)
- Added support and configuration for merging files in nested folders
- Added configuration for sorting mode (default/alphabetically)
## obsidian-advanced-merger 1.1.0
### Additions and Changes
- Fix issue where the created file will still be collected and merged
- Remove not needed await Promise.All
## obsidian-advanced-merger 1.0.2
### Additions and Changes
- Added sorting alphabetically by file path
## obsidian-advanced-merger 1.0.1
### Additions and Changes
- Remove unused onunload event
- Remove redundant file check

View file

@ -2,14 +2,13 @@ import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
@ -31,7 +30,8 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",

356
main.ts
View file

@ -1,75 +1,141 @@
import { App, Plugin, PluginSettingTab, Setting, TFile, TFolder } from "obsidian";
import {
App,
Modal,
Plugin,
PluginSettingTab,
Setting,
TFile,
TFolder,
Vault,
} from "obsidian";
const TRANSLATIONS: { [name: string]: { [name: string]: string } } = {
de: {
MergeFolder: "Ordner zusammenführen",
MergedFilesuffix: "zusammengeführt",
OverwriteFileQuestion: "Vorhandene Datei überschreiben",
Settings: "Einstellungen",
SettingSortAlphabetically: "Dateien alphabetisch sortieren",
SettingSortAlphabeticallyDescription: "Wenn aktiviert, werden die Dateien im ausgewählten Ordner alphabetisch nach ihrem vollständigen Pfad sortiert. Andernfalls werden die Dateien nach dem Erstellungsdatum sortiert (Standardverhalten).",
SettingSortAlphabeticallyDescription:
"Wenn aktiviert, werden die Dateien im ausgewählten Ordner alphabetisch nach ihrem vollständigen Pfad sortiert. Andernfalls werden die Dateien nach dem Erstellungsdatum sortiert (Standardverhalten).",
SettingIncludeNestedFolders: "Verschachtelte Ordner einbeziehen",
SettingIncludeNestedFoldersDescription: "Wenn aktiviert, werden Dateien in verschachtelten Ordnern zusammengeführt. Andernfalls werden nur Dateien im ausgewählten Ordner zusammengeführt (Standardverhalten)."
SettingIncludeNestedFoldersDescription:
"Wenn aktiviert, werden Dateien in verschachtelten Ordnern zusammengeführt. Andernfalls werden nur Dateien im ausgewählten Ordner zusammengeführt (Standardverhalten).",
Yes: "Ja",
No: "Nein",
},
en: {
MergeFolder: "Merge folder",
MergedFilesuffix: "merged",
OverwriteFileQuestion: "Overwite exising file",
Settings: "Settings",
SettingSortAlphabetically: "Sort files alphabetically",
SettingSortAlphabeticallyDescription: "If enabled, files in selected folder will be sorted alphabetically, according to its full path. Otherwise, files will be sorted by creation date (default behaviour).",
SettingSortAlphabeticallyDescription:
"If enabled, files in selected folder will be sorted alphabetically, according to its full path. Otherwise, files will be sorted by creation date (default behaviour).",
SettingIncludeNestedFolders: "Include nested folders",
SettingIncludeNestedFoldersDescription: "If enabled, files in nested folders will be included in merge. Otherwise, only files in selected folder will be merged (default behaviour)."
SettingIncludeNestedFoldersDescription:
"If enabled, files in nested folders will be included in merge. Otherwise, only files in selected folder will be merged (default behaviour).",
Yes: "Yes",
No: "No",
},
fi: {
MergeFolder: "Yhdistä kansio",
MergedFilesuffix: "yhdistetty",
OverwriteFileQuestion: "Korvaa olemassa oleva tiedosto",
Settings: "Asetukset",
SettingSortAlphabetically: "Lajittele tiedostot aakkosjärjestykseen",
SettingSortAlphabeticallyDescription: "Jos käytössä, valitun kansion tiedostot lajitellaan aakkosjärjestyksessä sen täyden polun mukaan. Muussa tapauksessa tiedostot lajitellaan luomispäivämäärän mukaan (oletuskäyttäytyminen).",
SettingSortAlphabeticallyDescription:
"Jos käytössä, valitun kansion tiedostot lajitellaan aakkosjärjestyksessä sen täyden polun mukaan. Muussa tapauksessa tiedostot lajitellaan luomispäivämäärän mukaan (oletuskäyttäytyminen).",
SettingIncludeNestedFolders: "Sisällytä sisäkkäiset kansiot",
SettingIncludeNestedFoldersDescription: "Jos käytössä, sisäkkäisten kansioiden tiedostot yhdistetään. Muussa tapauksessa vain valitun kansion tiedostot yhdistetään (oletustoiminto)."
SettingIncludeNestedFoldersDescription:
"Jos käytössä, sisäkkäisten kansioiden tiedostot yhdistetään. Muussa tapauksessa vain valitun kansion tiedostot yhdistetään (oletustoiminto).",
Yes: "Kyllä",
No: "Ei",
},
fr: {
MergeFolder: "Fusionner le dossier",
MergedFilesuffix: "fusionné",
OverwriteFileQuestion: "Remplacer le fichier existant",
Settings: "Paramètres",
SettingSortAlphabetically: "Trier les fichiers par ordre alphabétique",
SettingSortAlphabeticallyDescription: "Si activé, les fichiers du dossier sélectionné seront triés par ordre alphabétique, en fonction de son chemin complet. Sinon, les fichiers seront triés par date de création (comportement par défaut).",
SettingSortAlphabeticallyDescription:
"Si activé, les fichiers du dossier sélectionné seront triés par ordre alphabétique, en fonction de son chemin complet. Sinon, les fichiers seront triés par date de création (comportement par défaut).",
SettingIncludeNestedFolders: "Inclure les dossiers imbriqués",
SettingIncludeNestedFoldersDescription: "Si activé, les fichiers des dossiers imbriqués seront inclus dans la fusion. Sinon, seuls les fichiers du dossier sélectionné seront fusionnés (comportement par défaut)."
SettingIncludeNestedFoldersDescription:
"Si activé, les fichiers des dossiers imbriqués seront inclus dans la fusion. Sinon, seuls les fichiers du dossier sélectionné seront fusionnés (comportement par défaut).",
Yes: "Oui",
No: "Non",
},
ru: {
MergeFolder: "Объединить папку",
MergedFilesuffix: "совмещенный",
OverwriteFileQuestion: "Перезаписать существующий файл",
Settings: "Настройки",
SettingSortAlphabetically: "Сортировать файлы по алфавиту",
SettingSortAlphabeticallyDescription: "Если включено, файлы в выбранной папке будут отсортированы в алфавитном порядке в соответствии с полным путем. В противном случае, файлы будут отсортированы по дате создания (поведение по умолчанию)",
SettingSortAlphabeticallyDescription:
"Если включено, файлы в выбранной папке будут отсортированы в алфавитном порядке в соответствии с полным путём. В противном случае, файлы будут отсортированы по дате создания (поведение по умолчанию).",
SettingIncludeNestedFolders: "Влючать вложенные папки",
SettingIncludeNestedFoldersDescription: "Если включено, файлы во вложенных папках будут включены в слияние. В противном случае будут объединены только файлы в выбранной папке (поведение по умолчанию)."
SettingIncludeNestedFoldersDescription:
"Если включено, файлы во вложенных папках будут включены в слияние. В противном случае будут объединены только файлы в выбранной папке (поведение по умолчанию).",
Yes: "Да",
No: "Нет",
},
ua: {
MergeFolder: "Об'єднати папку",
MergedFilesuffix: "об'єднані",
OverwriteFileQuestion: "Перезаписати існуючий файл",
Settings: "Налаштування",
SettingSortAlphabetically: "Сортувати файли за алфавітом",
SettingSortAlphabeticallyDescription: "Якщо ввімкнено, файли у вибраній папці будуть відсортовані в алфавітному порядку відповідно до повного шляху. В іншому випадку файли будуть відсортовані за датою створення (поведінка за замовчуванням).",
SettingSortAlphabeticallyDescription:
"Якщо ввімкнено, файли у вибраній папці будуть відсортовані в алфавітному порядку відповідно до повного шляху. В іншому випадку файли будуть відсортовані за датою створення (поведінка за замовчуванням).",
SettingIncludeNestedFolders: "Включити вкладені папки",
SettingIncludeNestedFoldersDescription: "Якщо ввімкнено, файли у вкладених папках будуть включені в об’єднання. В іншому випадку буде об’єднано лише файли у вибраній папці (поведінка за замовчуванням)."
}
}
SettingIncludeNestedFoldersDescription:
"Якщо ввімкнено, файли у вкладених папках будуть включені в об’єднання. В іншому випадку буде об’єднано лише файли у вибраній папці (поведінка за замовчуванням).",
Yes: "Так",
No: "Ні",
},
};
interface AdvancedMergePluginSettings {
sortAlphabetically: boolean;
includeNestedFolders: boolean
includeNestedFolders: boolean;
}
const DEFAULT_SETTINGS: AdvancedMergePluginSettings = {
sortAlphabetically: false,
includeNestedFolders: false
}
includeNestedFolders: false,
};
/** @constant
@type {string}
@default
*/
const ICON_NAME = "git-merge";
/** @constant
@type {string}
@default
*/
const PLUGIN_NAME = "Advanced Merger";
/** @constant
@type {string}
@default
*/
const DEFAULT_LANGUAGE = "en";
/** @constant
@type {string}
@default
*/
const HANDLER_LOCATION = "file-menu";
/** @constant
@type {string}
@default
*/
const NEW_LINE_CHAR = "\n";
/** @constant
@type {string}
@default
*/
const DOUBLE_NEW_LINE_CHAR = "\n\n";
export default class AdvancedMerge extends Plugin {
public language: string;
@ -77,7 +143,10 @@ export default class AdvancedMerge extends Plugin {
public settings: AdvancedMergePluginSettings;
public async onload(): Promise<void> {
this.language = TRANSLATIONS[navigator.language] === null ? DEFAULT_LANGUAGE : navigator.language;
this.language =
TRANSLATIONS[navigator.language] === null
? DEFAULT_LANGUAGE
: navigator.language;
await this.loadSettings();
@ -90,8 +159,11 @@ export default class AdvancedMerge extends Plugin {
const folder = file;
menu.addItem((item) => {
item.setTitle(TRANSLATIONS[this.language].MergeFolder)
.setIcon("git-merge")
.onClick(async(evt) => await this.onClickCallback(folder, evt));
.setIcon(ICON_NAME)
.onClick(
async (evt) =>
await this.onClickCallback(folder, evt)
);
});
})
);
@ -100,55 +172,122 @@ export default class AdvancedMerge extends Plugin {
this.addSettingTab(new AdvancedMergeSettingTab(this.app, this));
}
private async onClickCallback(folder: TFolder, _evt: MouseEvent | KeyboardEvent): Promise<void> {
private async onClickCallback(
folder: TFolder,
_evt: MouseEvent | KeyboardEvent
): Promise<void> {
const { vault } = this.app;
const files = this.fileSort(
vault
.getMarkdownFiles()
.filter((file) => this.fileFilter(folder, file)));
const destination = await vault.create(
`${folder.path}-${TRANSLATIONS[this.language].MergedFilesuffix}.md`,
""
.filter((file) => this.fileFilter(folder, file))
);
files.forEach(async (file) => {
const mergedFileName = `${folder.path}-${
TRANSLATIONS[this.language].MergedFilesuffix
}.md`;
const fileExists = await vault.adapter.exists(mergedFileName, false);
if (fileExists) {
new AdvancedMergeOverwriteFileModal(
this.app,
this.language,
mergedFileName,
async (deleteFile) => {
if (!deleteFile) {
console.info(
`file "${mergedFileName}" already exists, but user cancelled deleting..`
);
return;
}
console.info(
`file "${mergedFileName}" already exists, deleting..`
);
await vault.adapter.remove(mergedFileName);
await this.mergeFiles(vault, files, mergedFileName);
}
).open();
return;
}
await this.mergeFiles(vault, files, mergedFileName);
}
/**
* Merges input files to single output file.
* @param {Vault} vault - Current vault.
* @param {Array<TFile>} files - Files, to be included.
* @param {string} mergedFileName - Output file name.
*/
private async mergeFiles(
vault: Vault,
files: Array<TFile>,
mergedFileName: string
): Promise<void> {
const destination = await vault.create(mergedFileName, "");
files.forEach(async (file: TFile, index: number) => {
let contents = await vault.read(file);
contents = `\n# ${file.name.replace(
/\.md$/,
""
)}\n${contents}\n`;
const fileSectionName = file.name.replace(/\.md$/, "");
// For the first file in a row, we shouldnt add new line
contents = `${
index === 0 ? "" : NEW_LINE_CHAR
}# ${fileSectionName}${DOUBLE_NEW_LINE_CHAR}${contents}${NEW_LINE_CHAR}`;
console.info(
`Adding file "${file.name}" as section "${fileSectionName}" into file "${mergedFileName}"..`
);
vault.append(destination, contents);
});
}
/**
* Applies filtering to input file.
* @param {TFolder} folder - Selected folder.
* @param {TFile} file - Files, to be included.
* @returns {boolean} Provided file passses folder check.
*/
private fileFilter(folder: TFolder, file: TFile): boolean {
return this.settings.includeNestedFolders ?
!!file.parent?.path.startsWith(folder.path)
return this.settings.includeNestedFolders
? !!file.parent?.path.startsWith(folder.path)
: file.parent?.path == folder.path;
}
/**
* Applies sorting to input files array.
* @param {Array<TFile>} files - The files array, to be sorted.
* @returns {Array<TFile>} Sorted files array.
*/
private fileSort(files: Array<TFile>): Array<TFile> {
return this.settings.sortAlphabetically ?
files.sort((a, b) => {
const x = a.path.toLowerCase();
const y = b.path.toLowerCase();
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
})
return this.settings.sortAlphabetically
? files.sort((a, b) => {
const x = a.path.toLowerCase();
const y = b.path.toLowerCase();
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
})
: files.reverse();
}
/**
* De-serializes settings from persistent storage.
*/
private async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
/**
* Serializes settings into persistent storage.
*/
public async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
@ -157,36 +296,123 @@ export default class AdvancedMerge extends Plugin {
class AdvancedMergeSettingTab extends PluginSettingTab {
private plugin: AdvancedMerge;
/**
* Represents a settings tab.
* @constructor
* @param {App} app - The `Obsidian` application object.
* @param {AdvancedMerge} plugin - The `AdvancedMerge` plugin object.
*/
constructor(app: App, plugin: AdvancedMerge) {
super(app, plugin);
this.plugin = plugin;
}
public display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {text: `${PLUGIN_NAME} - ${TRANSLATIONS[this.plugin.language].Settings}`});
containerEl.createEl("h2", {
text: `${PLUGIN_NAME} - ${
TRANSLATIONS[this.plugin.language].Settings
}`,
});
// Add "sort alphabetically" toggle in settings
new Setting(containerEl)
.setName(TRANSLATIONS[this.plugin.language].SettingSortAlphabetically)
.setDesc(TRANSLATIONS[this.plugin.language].SettingSortAlphabeticallyDescription)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.sortAlphabetically)
.onChange(async (value) => {
this.plugin.settings.sortAlphabetically = value;
await this.plugin.saveSettings();
}));
.setName(
TRANSLATIONS[this.plugin.language].SettingSortAlphabetically
)
.setDesc(
TRANSLATIONS[this.plugin.language]
.SettingSortAlphabeticallyDescription
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.sortAlphabetically)
.onChange(async (value) => {
this.plugin.settings.sortAlphabetically = value;
await this.plugin.saveSettings();
})
);
// Add "include nested folders" toggle in settings
new Setting(containerEl)
.setName(TRANSLATIONS[this.plugin.language].SettingIncludeNestedFolders)
.setDesc(TRANSLATIONS[this.plugin.language].SettingIncludeNestedFoldersDescription)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeNestedFolders)
.onChange(async (value) => {
this.plugin.settings.includeNestedFolders = value;
await this.plugin.saveSettings();
}));
.setName(
TRANSLATIONS[this.plugin.language].SettingIncludeNestedFolders
)
.setDesc(
TRANSLATIONS[this.plugin.language]
.SettingIncludeNestedFoldersDescription
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.includeNestedFolders)
.onChange(async (value) => {
this.plugin.settings.includeNestedFolders = value;
await this.plugin.saveSettings();
})
);
}
}
class AdvancedMergeOverwriteFileModal extends Modal {
private language: string;
private existingFileName: string;
private onSubmitHandler: (result: boolean) => void;
/**
* Represents an "Overwrite file?" dialog.
* @constructor
* @param {App} app - The `Obsidian` application object.
* @param {string} language - Application language.
* @param {string} existingFileName - Existing file name.
* @param handler - Modal callback handler.
*/
constructor(
app: App,
language: string,
existingFileName: string,
handler: (result: boolean) => void
) {
super(app);
this.language = language;
this.existingFileName = existingFileName;
this.onSubmitHandler = handler;
}
public onOpen() {
const { contentEl } = this;
contentEl.createEl("h3", {
text: `${TRANSLATIONS[this.language].OverwriteFileQuestion} "${
this.existingFileName
}"?`,
});
new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText(TRANSLATIONS[this.language].No)
.setCta()
.onClick(() => {
this.close();
this.onSubmitHandler(false);
})
)
.addButton((btn) =>
btn
.setButtonText(TRANSLATIONS[this.language].Yes)
.setCta()
.onClick(() => {
this.close();
this.onSubmitHandler(true);
})
);
}
public onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -1,7 +1,7 @@
{
"id": "advanced-merger",
"name": "Advanced Merger",
"version": "1.2.0",
"version": "1.3.0",
"minAppVersion": "0.15.0",
"description": "Merge a folder of notes for easier export.",
"author": "Anto Keinänen",

View file

@ -1,7 +1,21 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "obsidian-advanced-merger",
"version": "1.3.0",
"description": "Merge a folder of notes for easier export. This plugin is designed for Obsidian (https://obsidian.md).",
"homepage": "https://github.com/antoKeinanen/obsidian-advanced-merger",
"contributors": [
{
"name": "Dmitry Besedin",
"email": "endarvaluk@gmail.com",
"url": "https://github.com/EndarValuk/"
}
],
"funding": [
{
"type": "individual",
"url": "https://www.buymeacoffee.com/antokeinanen"
}
],
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -10,7 +24,10 @@
"prepare": "husky install"
},
"keywords": [],
"author": "",
"author": {
"name": "Anto Keinänen",
"url": "https://github.com/antoKeinanen/"
},
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",