feature() saving work on the models and the parser

This commit is contained in:
Roland Broekema 2025-03-08 18:55:18 +01:00
parent 619a286f80
commit 8a164c6ae7
5 changed files with 135 additions and 58 deletions

17
package-lock.json generated
View file

@ -1,14 +1,15 @@
{
"name": "obsidian-contacts",
"version": "1.0.0",
"version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-contacts",
"version": "1.0.0",
"version": "1.2.0",
"license": "MIT",
"dependencies": {
"js-yaml": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@ -460,9 +461,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"peer": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-union": {
"version": "2.1.0",
@ -1560,8 +1559,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -2529,9 +2526,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"peer": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-union": {
"version": "2.1.0",
@ -3275,8 +3270,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"peer": true,
"requires": {
"argparse": "^2.0.1"
}

View file

@ -24,7 +24,8 @@
"typescript": "4.7.4"
},
"dependencies": {
"js-yaml": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}

View file

@ -1,6 +1,8 @@
import { normalizePath, Notice, TFile, TFolder, Vault, Workspace } from "obsidian";
import {App, Modal, normalizePath, Notice, TFile, TFolder, Vault, Workspace} from "obsidian";
import { join } from "path";
import { Template } from "src/settings/settings";
import { ContactNameModal } from "../modals/contactNameModal";
import {FileExistsModal} from "../modals/fileExistsModal";
const customFormat =
`/---contact---/
@ -45,45 +47,51 @@ export function findContactFiles(contactsFolder: TFolder) {
return contactFiles;
}
export function createContactFile(folderPath: string, template: Template, hashtag: string, vault: Vault, workspace: Workspace) {
const folder = vault.getAbstractFileByPath(folderPath)
if (!folder) {
new Notice(`Can not find path: '${folderPath}'. Please update "Contacts" plugin settings`);
return;
}
export function createContactFile(app: App, folderPath: string, template: Template, hashtag: string, vault: Vault, workspace: Workspace) {
const folder = vault.getAbstractFileByPath(folderPath)
if (!folder) {
new Notice(`Can not find path: '${folderPath}'. Please update "Contacts" plugin settings`);
return;
}
vault.create(normalizePath(join(folderPath, `Contact ${findNextFileNumber(folderPath, vault)}.md`)), getNewFileContent(template, hashtag))
.then(createdFile => openFile(createdFile, workspace));
new ContactNameModal(app, async (givenName, familyName) => {
const filePath = normalizePath(join(folderPath, `${givenName} ${familyName}.md`));
const fileExists = await vault.adapter.exists(filePath);
if (fileExists) {
new FileExistsModal(filePath, (action: "replace" | "skip") => {
if(action === "skip") {
new Notice("File creation skipped.");
}
if(action === "replace") {
vault.adapter.write(filePath, getNewFileContent(template, hashtag))
.then(() => {
const file = vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
openFile(file, workspace);
}
new Notice(`File overwritten.`);
});
}
}).open();
} else {
vault.create(filePath, getNewFileContent(template, hashtag))
.then(createdFile => openFile(createdFile, workspace));
}
}).open();
}
function findNextFileNumber(folderPath: string, vault: Vault) {
const folder = vault.getAbstractFileByPath(
normalizePath(folderPath)
) as TFolder;
// export function createContactFileWithData() {
// const folder = vault.getAbstractFileByPath(folderPath)
// if (!folder) {
// new Notice(`Can not find path: '${folderPath}'. Please update "Contacts" plugin settings`);
// return;
// }
//
// vault.create(normalizePath(join(folderPath, `Contact ${findNextFileNumber(folderPath, vault)}.md`)), getNewFileContent(template, hashtag))
// .then(createdFile => openFile(createdFile, workspace));
// }
let nextNumber = 0;
Vault.recurseChildren(folder, (contactNote) => {
if (!(contactNote instanceof TFile)) {
return;
}
const name = contactNote.basename;
const regex = /Contact(?<number>\s\d+)*/g;
for (const match of name.matchAll(regex)) {
if (!match.groups || !match.groups.number) {
if (nextNumber === 0) {
nextNumber = 1;
}
continue;
}
const currentNumberString = match.groups.number.trim();
if (currentNumberString != undefined && currentNumberString !== "") {
const currentNumber = parseInt(currentNumberString);
nextNumber = Math.max(nextNumber, (currentNumber + 1));
}
}
});
return nextNumber === 0 ? "" : nextNumber.toString();
}
function getNewFileContent(template: Template, hashtag: string): string {
let hashtagSuffix = '';
@ -98,4 +106,38 @@ function getNewFileContent(template: Template, hashtag: string): string {
default:
return customFormat + hashtagSuffix;
}
}
}
export async function openFilePicker(type: string): Promise<string> {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.accept = type;
input.style.display = "none";
input.addEventListener("change", () => {
if (input?.files && input.files.length > 0) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = function (event) {
const rawData = event?.target?.result || '';
if (typeof rawData === "string") {
resolve(rawData);
} else {
resolve(new TextDecoder("utf-8").decode(rawData));
}
};
reader.readAsText(file, "UTF-8");
} else {
resolve('');
}
});
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
});
}

View file

@ -6,6 +6,8 @@ type HeaderProps = {
onSortChange: React.Dispatch<React.SetStateAction<Sort>>;
sort: Sort;
onCreateContact: () => void;
importVCF: () => void;
exportVCF: () => void;
};
export const HeaderView = (props: HeaderProps) => {
@ -22,11 +24,33 @@ export const HeaderView = (props: HeaderProps) => {
<div
id="create-btn"
className="clickable-icon nav-action-button"
data-icon="contact"
aria-label="Create New Contact"
ref={(element) => (buttons.current[1] = element)}
onClick={props.onCreateContact}
>
Create
</div>
/>
<div
id="import-vcf-btn"
data-icon="file-down"
className={
"clickable-icon nav-action-button "
}
aria-label="import VCF"
ref={(element) => (buttons.current[2] = element)}
onClick={ props.importVCF }
/>
<div
id="import-vcf-btn"
data-icon="file-up"
className={
"clickable-icon nav-action-button "
}
aria-label="export VCF"
ref={(element) => (buttons.current[3] = element)}
onClick={ props.exportVCF }
/>
<div className="vl"></div>
<div
id="sort-by-name-btn"
@ -36,7 +60,7 @@ export const HeaderView = (props: HeaderProps) => {
(props.sort === Sort.NAME && "is-active")
}
aria-label="Sort By Name"
ref={(element) => (buttons.current[1] = element)}
ref={(element) => (buttons.current[4] = element)}
onClick={() => props.onSortChange(Sort.NAME)}
/>
<div
@ -47,7 +71,7 @@ export const HeaderView = (props: HeaderProps) => {
(props.sort === Sort.LAST_CONTACT && "is-active")
}
aria-label="Sort By Last Contact"
ref={(element) => (buttons.current[2] = element)}
ref={(element) => (buttons.current[5] = element)}
onClick={() => props.onSortChange(Sort.LAST_CONTACT)}
/>
<div
@ -58,7 +82,7 @@ export const HeaderView = (props: HeaderProps) => {
(props.sort === Sort.BIRTHDAY && "is-active")
}
aria-label="Sort By Birthday"
ref={(element) => (buttons.current[3] = element)}
ref={(element) => (buttons.current[6] = element)}
onClick={() => props.onSortChange(Sort.BIRTHDAY)}
/>
</div>

View file

@ -1,20 +1,22 @@
import { normalizePath, TAbstractFile, TFile, TFolder } from "obsidian";
import * as React from "react";
import { useApp } from "src/context/hooks";
import { createContactFile, findContactFiles } from "src/file/file";
import { createContactFile, findContactFiles, openFilePicker } from "src/file/file";
import ContactsPlugin from "src/main";
import { Contact } from "src/parse/contact";
import { parseContactFiles } from "src/parse/parse";
import { Sort } from "src/util/constants";
import { ContactsListView } from "./ContactsListView";
import { HeaderView } from "./HeaderView";
import { parseToSingles, parseVcard } from "src/parse/vcard/vcardParse";
type RootProps = {
plugin: ContactsPlugin;
};
export const SidebarRootView = (props: RootProps) => {
const { vault, metadataCache, workspace } = useApp();
const app = useApp();
const { vault, metadataCache, workspace } = app;
const [contacts, setContacts] = React.useState<Contact[]>([]);
const [sort, setSort] = React.useState<Sort>(Sort.LAST_CONTACT);
const folder = props.plugin.settings.contactsFolder;
@ -67,8 +69,23 @@ export const SidebarRootView = (props: RootProps) => {
<div>
<HeaderView
onSortChange={setSort}
importVCF={() => {
openFilePicker('.vcf').then((fileContent: string) => {
if (fileContent === '') {
return;
} else {
const singles: string[] = parseToSingles(fileContent);
for (const single of singles) {
const vcardData = parseVcard(single);
}
}
})
}}
exportVCF={() => {}}
onCreateContact={() =>
createContactFile(
app,
folder,
props.plugin.settings.template,
props.plugin.settings.defaultHashtag,