fix now using ful names for file name creation and resolving naming order issue

This commit is contained in:
Roland Broekema 2025-04-06 18:54:49 +02:00
parent 991e8d634c
commit ff288eac18
9 changed files with 143 additions and 25 deletions

View file

@ -121,3 +121,18 @@ export function saveVcardFilePicker(data: string, obsidianFile?: TFile ) {
element.click();
} catch (_err) {}
}
export function createFileName(records: Record<string, string>) {
const parts = [
records['N.PREFIX'] || '',
records['N.GN'] || '',
records['N.MN'] || '',
records['N.FN'] || '',
records['N.SUFFIX'] || ''
];
return parts
.map(part => part.trim())
.filter(part => part !== '')
.join(' ') + '.md';
}

View file

@ -31,7 +31,7 @@ export enum VCardSupportedKey {
}
export const VCardStructuredFields = {
N: ["PREFIX", "GN", "MN", "FN", "SUFFIX"],
N: ["FN", "GN", "MN", "PREFIX", "SUFFIX"],
ADR: ["PO", "EXT", "STREET", "LOCALITY", "REGION", "POSTAL", "COUNTRY"]
} as const;

View file

@ -1,5 +1,6 @@
import {VCardForObsidianRecord, VCardStructuredFields} from "./vcardDefinitions";
import { ContactNameModal } from "src/ui/modals/contactNameModal";
import { convertToLatestVCFPhotoFormat } from "src/util/avatarActions";
function unfoldVCardLines(vCardData: string): string[] {
const lines = vCardData.split(/\r\n?/g);
@ -129,6 +130,7 @@ function indexIfKeysExist(vCardObject: VCardForObsidianRecord, newEntry: VCardFo
while (vCardObject.hasOwnProperty(newKey)) {
index++;
if (typeRegex.test(key)) {
newKey = key.replace(typeRegex, `[${index}:$1]`);
} else if (dotRegex.test(key)) {
@ -164,6 +166,7 @@ function parseVCardLine(line: string): VCardForObsidianRecord {
const [key, ...valueParts] = line.split(":");
const value = valueParts.join(":").trim();
const [property, ...paramParts] = key.split(";");
const params = paramParts.reduce((acc, part) => {
const [paramKey, paramValue] = part.split("=");
acc[paramKey.toLowerCase()] = paramValue ? paramValue.split(",") : [];
@ -174,7 +177,11 @@ function parseVCardLine(line: string): VCardForObsidianRecord {
let parsedData: Record<string, any> = {};
const typeValues:string = params["type"] ? `[${params["type"].join(",")}]` : "";
if (propKey in VCardStructuredFields) {
if (key.contains('PHOTO') && key.contains('ENCODING=BASE64')) {
parsedData['PHOTO'] = convertToLatestVCFPhotoFormat(line);
} else if (key === 'VERSION') {
parsedData['VERSION'] = '4.0';
} else if (propKey in VCardStructuredFields) {
parsedData = parseStructuredField(propKey as keyof typeof VCardStructuredFields, value, typeValues);
} else if (propKey in ['BDAY', 'ANNIVERSARY']) {
parsedData[`${propKey}${typeValues}`] = formatVCardDate(value)

View file

@ -1,4 +1,5 @@
import * as React from "react";
import { convertToLatestVCFPhotoFormat } from "src/util/avatarActions";
interface AvatarProps {
photoUrl?: string;
@ -14,7 +15,7 @@ export const Avatar = (props: AvatarProps) => {
<div className="avatar-initials">
{props.photoUrl && !hasImageError ? (
<img
src={props.photoUrl}
src={convertToLatestVCFPhotoFormat(props.photoUrl)}
onError={() => setHasImageError(true)}
/>
) : (

View file

@ -10,6 +10,7 @@ import { CopyableItem } from "./CopyableItem";
type ContactProps = {
contact: Contact;
exportVCF: (contactFile: TFile) => void;
processAvatar: (contact: Contact) => void;
};
@ -129,7 +130,6 @@ export const ContactView = (props: ContactProps) => {
id={fileId(contact.file)}
>
<div className="content">
<div className="inner-card-container">
<div className="bizzy-card-container">
{renderOrganization(contact.data)}
@ -138,7 +138,8 @@ export const ContactView = (props: ContactProps) => {
<Avatar photoUrl={contact.data["PHOTO"]} firstName={contact.data["N.GN"]}
lastName={contact.data["N.FN"]}/>
<div className="biz-words-container">
<div className="biz-name"> {[
<div className="biz-name">
{[
contact.data["N.PREFIX"],
contact.data["N.GN"],
contact.data["N.MN"],
@ -159,7 +160,8 @@ export const ContactView = (props: ContactProps) => {
</div>
<div className="biz-card-b">
<div className="biz-shape"/>
<div className="biz-shape">
</div>
<div className="biz-contact-box">
{renderFirstPhone(['TEL[CELL]', 'TEL'], contact.data)}
{renderFirstPhone(['TEL[WORK]', 'TEL[HOME]'], contact.data)}
@ -167,16 +169,29 @@ export const ContactView = (props: ContactProps) => {
{renderFirstEmail(['EMAIL', 'EMAIL[WORK]'], contact.data)}
{renderFirstAdress(['ADR[WORK]', 'ADR[HOME]', 'ADR'], contact.data)}
</div>
</div>
<div
data-icon="file-up"
className={
"clickable-icon nav-action-button "
}
aria-label="export VCF"
ref={(element) => (buttons.current[0] = element)}
onClick={() => props.exportVCF(contact.file)}
/>
</div>
<div className="biz-contact-actions">
<div
data-icon="image-up"
className={
"clickable-icon nav-action-button "
}
aria-label="Process Avatar"
ref={(element) => (buttons.current[0] = element)}
onClick={() => props.processAvatar(contact)}
>
</div>
<div
data-icon="file-up"
className={
"clickable-icon nav-action-button "
}
aria-label="export VCF"
ref={(element) => (buttons.current[1] = element)}
onClick={() => props.exportVCF(contact.file)}
>
</div>
</div>
</div>
</div>
</div>

View file

@ -10,6 +10,7 @@ import {TFile} from "obsidian";
type ContactsListProps = {
contacts: Contact[];
sort: Sort;
processAvatar: (contact: Contact) => void;
exportVCF: (contactFile: TFile) => void;
};
@ -94,7 +95,7 @@ export const ContactsListView = (props: ContactsListProps) => {
return (
<>
{processedContacts.map((contact) => {
return <ContactView contact={contact} key={randomUUID()} exportVCF={props.exportVCF} />;
return <ContactView contact={contact} key={randomUUID()} exportVCF={props.exportVCF} processAvatar={props.processAvatar} />;
})}
</>
);

View file

@ -1,7 +1,7 @@
import { normalizePath, TAbstractFile, TFile, TFolder } from "obsidian";
import * as React from "react";
import { useApp } from "src/context/hooks";
import {createContactFile, findContactFiles, openFilePicker, saveVcardFilePicker} from "src/file/file";
import {createContactFile, createFileName, findContactFiles, openFilePicker, saveVcardFilePicker} from "src/file/file";
import ContactsPlugin from "src/main";
import { Contact } from "src/parse/contact";
import { parseContactFiles } from "src/parse/parse";
@ -12,7 +12,7 @@ import { createEmptyVcard, parseToSingles, parseVcard } from "src/parse/vcard/vc
import { mdRender } from "src/parse/vcard/vcardMdTemplate";
import myScrollTo from "src/util/myScrollTo";
import { vcardToString } from "src/parse/vcard/vcardToString";
import { processAvatar } from "src/util/avatarActions";
type RootProps = {
plugin: ContactsPlugin;
@ -93,30 +93,32 @@ export const SidebarRootView = (props: RootProps) => {
const singles: string[] = parseToSingles(fileContent);
for (const single of singles) {
const records = await parseVcard(single);
const fileName = `${records['N.GN']} ${records['N.FN']}.md`
const mdContent = mdRender(records, props.plugin.settings.defaultHashtag);
createContactFile(app, folder, mdContent, fileName)
createContactFile(app, folder, mdContent, createFileName(records))
}
}
})
}}
exportAllVCF={async() => {
const allContactFiles = contacts.map((contact)=> contact.file)
const vcards = await vcardToString(metadataCache, allContactFiles);
saveVcardFilePicker(vcards)
}}
onCreateContact={async () => {
const records = await createEmptyVcard();
const fileName = `${records['N.GN']} ${records['N.FN']}.md`
const mdContent = mdRender(records, props.plugin.settings.defaultHashtag);
createContactFile(app, folder, mdContent, fileName)
createContactFile(app, folder, mdContent, createFileName(records))
}}
sort={sort}
/>
<ContactsListView
contacts={contacts}
sort={sort}
processAvatar={(contact :Contact) => {
(async () => {
await processAvatar(contact)
})();
}}
exportVCF={(contactFile: TFile) => {
(async () => {
const vcards = await vcardToString(metadataCache, [contactFile])

73
src/util/avatarActions.ts Normal file
View file

@ -0,0 +1,73 @@
import {Contact} from "../parse/contact";
const resizeAndCropImage = (img: HTMLImageElement, outputSize: number): HTMLCanvasElement => {
const canvas = document.createElement('canvas');
canvas.width = outputSize;
canvas.height = outputSize;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas context not available');
const { naturalWidth: srcW, naturalHeight: srcH } = img;
const scale = Math.max(outputSize / srcW, outputSize / srcH);
const scaledW = srcW * scale;
const scaledH = srcH * scale;
const dx = (outputSize - scaledW) / 2;
const dy = (outputSize - scaledH) / 2;
ctx.drawImage(img, dx, dy, scaledW, scaledH);
return canvas;
};
const base64EncodeImage = (canvas: HTMLCanvasElement, quality: number = 1): string => {
return canvas.toDataURL('image/jpeg', quality);
};
const getImage = (fileOrUrl: File | string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = reject;
if (typeof fileOrUrl === 'string') {
img.src = fileOrUrl;
} else {
const reader = new FileReader();
reader.onload = () => {
img.src = reader.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(fileOrUrl);
}
});
};
const setBase64Avatar = async (
fileOrUrl: File | string,
): Promise<string> => {
const outputSize = 120;
const quality = 1;
const img = await getImage(fileOrUrl);
const canvas = resizeAndCropImage(img, outputSize);
return base64EncodeImage(canvas, quality);
};
export const processAvatar = async (contact: Contact) => {
console.log(contact.data['PHOTO']);
}
export const convertToLatestVCFPhotoFormat = (line:string) => {
const url = line.startsWith('PHOTO;') ? line.slice(6) : line;
const match = url.match(/^ENCODING=BASE64;(.*?):/);
if (match) {
const mimeType = match[1].toLowerCase(); // e.g., "jpeg"
const base64Data = url.split(':').slice(1).join(':');
return `data:image/${mimeType};base64,${base64Data}`;
}
return url;
}

View file

@ -72,10 +72,14 @@
}
}
.bizzy-card-container .nav-action-button {
.bizzy-card-container .biz-contact-actions {
position: absolute;
right: 4px;
top: 4px;
display: flex;
}
.bizzy-card-container .biz-contact-actions .clickable-icon {
padding: var(--size-2-2);
}
/*=============================