mirror of
https://github.com/broekema41/obsidian-vcf-contacts.git
synced 2026-07-22 05:42:58 +00:00
fix now using ful names for file name creation and resolving naming order issue
This commit is contained in:
parent
991e8d634c
commit
ff288eac18
9 changed files with 143 additions and 25 deletions
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
73
src/util/avatarActions.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/*=============================
|
||||
|
|
|
|||
Loading…
Reference in a new issue