mirror of
https://github.com/broekema41/obsidian-vcf-contacts.git
synced 2026-07-22 05:42:58 +00:00
Refs #8: after a couple hours more work we can now communicate with the carddav server to get metadata push and pull
This commit is contained in:
parent
0810c8af51
commit
9fda442a9e
14 changed files with 333 additions and 66 deletions
|
|
@ -28,3 +28,4 @@ While the book doesn’t define a testing method, it strongly influences how we
|
||||||
- **Example:**
|
- **Example:**
|
||||||
- Move validation, data transformation, and API interaction logic from UI components to `settings.ts` or a dedicated service module.
|
- Move validation, data transformation, and API interaction logic from UI components to `settings.ts` or a dedicated service module.
|
||||||
- UI components should only call functions from these modules and display results.
|
- UI components should only call functions from these modules and display results.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ function extractSubkey(input: string): { main: string; subkey?: string } {
|
||||||
* Otherwise, it assumes the content is a type.
|
* Otherwise, it assumes the content is a type.
|
||||||
*/
|
*/
|
||||||
function parseBracketContent(content: string): { index?: string; type?: string } {
|
function parseBracketContent(content: string): { index?: string; type?: string } {
|
||||||
if (content.includes(':')) {
|
if (content.includes(':')) {
|
||||||
const [index, type] = content.split(':');
|
const [index, type] = content.split(':');
|
||||||
return { index, type };
|
return { index, type };
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +81,6 @@ function parseKeyPart(main: string): { key: string; index?: string; type?: strin
|
||||||
// Extract and parse the content within the brackets.
|
// Extract and parse the content within the brackets.
|
||||||
const bracketContent = main.substring(openBracketIndex + 1, closeBracketIndex);
|
const bracketContent = main.substring(openBracketIndex + 1, closeBracketIndex);
|
||||||
const { index, type } = parseBracketContent(bracketContent);
|
const { index, type } = parseBracketContent(bracketContent);
|
||||||
|
|
||||||
return { key, index, type };
|
return { key, index, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,6 @@ function generateVCard(file: TFile): string {
|
||||||
const singleLineFields: Array<[string, string]> = [];
|
const singleLineFields: Array<[string, string]> = [];
|
||||||
const structuredFields: Array<[string, string]> = [];
|
const structuredFields: Array<[string, string]> = [];
|
||||||
|
|
||||||
singleLineFields.push(['FN', file.basename]);
|
|
||||||
|
|
||||||
entries.forEach(([key, value]) => {
|
entries.forEach(([key, value]) => {
|
||||||
const keyObj = parseKey(key);
|
const keyObj = parseKey(key);
|
||||||
|
|
||||||
|
|
@ -62,6 +60,10 @@ function generateVCard(file: TFile): string {
|
||||||
singleLineFields.push([key, value]);
|
singleLineFields.push([key, value]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!singleLineFields.some(([key]) => key === 'FN')) {
|
||||||
|
singleLineFields.push(['FN', file.basename]);
|
||||||
|
}
|
||||||
|
|
||||||
const structuredLines = renderStructuredLines(structuredFields);
|
const structuredLines = renderStructuredLines(structuredFields);
|
||||||
const singleLines = singleLineFields.map(renderSingleKey);
|
const singleLines = singleLineFields.map(renderSingleKey);
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,9 @@ import * as React from "react";
|
||||||
import { Contact, updateFrontMatterValue } from "src/contacts";
|
import { Contact, updateFrontMatterValue } from "src/contacts";
|
||||||
import { getSettings } from "src/context/sharedSettingsContext";
|
import { getSettings } from "src/context/sharedSettingsContext";
|
||||||
import { InsightProcessor, InsightQueItem, RunType } from "src/insights/insight.d";
|
import { InsightProcessor, InsightQueItem, RunType } from "src/insights/insight.d";
|
||||||
|
import { generateUUID } from "src/util/vcard";
|
||||||
|
|
||||||
|
|
||||||
// Zero dependency uuid generator as its not used for millions of records
|
|
||||||
const generateUUID = (): string => {
|
|
||||||
const timestamp = Date.now().toString(16).padStart(12, '0');
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
||||||
const r = Math.random() * 16 | 0;
|
|
||||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
}).replace(/^(.{24})/, (_, p1) => {
|
|
||||||
return timestamp + p1.slice(timestamp.length);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderGroup = (queItems: InsightQueItem[]):JSX.Element => {
|
const renderGroup = (queItems: InsightQueItem[]):JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
14
src/sync/adapters/adapter.d.ts
vendored
14
src/sync/adapters/adapter.d.ts
vendored
|
|
@ -1,12 +1,12 @@
|
||||||
export interface VCardRaw {
|
export interface VCardRaw {
|
||||||
uid: string;
|
uid: string | undefined;
|
||||||
raw: string; // The raw VCF data
|
raw: string; // The raw VCF data
|
||||||
// Optionally add parsed fields if desired (name, email, etc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VCardMeta {
|
export interface VCardMeta {
|
||||||
uid: string;
|
href: string;
|
||||||
name?: string;
|
etag: string;
|
||||||
lastModified: string; // ISO string, always present
|
lastModified: Date;
|
||||||
// Optionally more metadata (e.g., email, phone, photo presence, etc)
|
uid: string | undefined;
|
||||||
|
fn: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,19 @@ export interface AdapterInterface {
|
||||||
* @param uid The unique identifier of the vCard to pull
|
* @param uid The unique identifier of the vCard to pull
|
||||||
* @returns Promise resolving to the vCard (or null if not found)
|
* @returns Promise resolving to the vCard (or null if not found)
|
||||||
*/
|
*/
|
||||||
pull(uid: string): Promise<VCardRaw | undefined>;
|
pull(href: string): Promise<VCardRaw | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of all vCard metadata (UID, name, lastModified, etc).
|
* Get a list of all vCard metadata (UID, name, lastModified, etc).
|
||||||
* @returns Promise resolving to an array of vCard metadata objects
|
* @returns Promise resolving to an array of vCard metadata objects
|
||||||
*/
|
*/
|
||||||
getList(): Promise<VCardMeta[]>;
|
getMetaList(): Promise<VCardMeta[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a vCard metadata (UID, name, lastModified, etc).
|
||||||
|
* @returns Promise resolving to an array of vCard metadata objects
|
||||||
|
*/
|
||||||
|
getMetaByUid(uid: string): Promise<VCardMeta|undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the remote party is reachable with current settings.
|
* Check if the remote party is reachable with current settings.
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,22 @@
|
||||||
import { getSettings, onSettingsChange } from "src/context/sharedSettingsContext";
|
import { getSettings } from "src/context/sharedSettingsContext";
|
||||||
|
import { ContactsPluginSettings } from "src/settings/settings";
|
||||||
import { VCardMeta, VCardRaw } from "src/sync/adapters/adapter";
|
import { VCardMeta, VCardRaw } from "src/sync/adapters/adapter";
|
||||||
import { AdapterInterface } from "src/sync/adapters/adapterInterface";
|
import { AdapterInterface } from "src/sync/adapters/adapterInterface";
|
||||||
|
import { getList } from "src/sync/singlePull";
|
||||||
import { CarddavSettingsInterface } from "src/ui/settings/components/carddavSettings";
|
import { CarddavSettingsInterface } from "src/ui/settings/components/carddavSettings";
|
||||||
import { AppHttpResponse, PlatformHttpClient } from "src/util/platformHttpClient";
|
import { AppHttpResponse, PlatformHttpClient } from "src/util/platformHttpClient";
|
||||||
|
import { fnOutOfString, uidOutOfString } from "src/util/vcard";
|
||||||
|
|
||||||
|
|
||||||
export function carddavGenericAdapter(): AdapterInterface {
|
export function carddavGenericAdapter(): AdapterInterface {
|
||||||
|
|
||||||
let settings = getSettings();
|
|
||||||
onSettingsChange(()=> {
|
|
||||||
settings = getSettings();
|
|
||||||
});
|
|
||||||
|
|
||||||
const getAuthHeader = () => {
|
const getAuthHeader = (settings:ContactsPluginSettings) => {
|
||||||
return 'Bearer ' + this.bearerToken;
|
if (settings.CardDAV.authType === 'apikey') {
|
||||||
}
|
return `Bearer ${settings.CardDAV.authKey}`;
|
||||||
|
} else {
|
||||||
const doFetch = async (options: RequestInit): Promise<Response> =>{
|
return `Basic ${settings.CardDAV.authKey}`;
|
||||||
return fetch(settings.CardDAV.addressBookUrl, {
|
}
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options.headers,
|
|
||||||
['Authorization']: this.getAuthHeader()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const doPush = async (options: RequestInit, vcard: VCardRaw): Promise<void> => {
|
|
||||||
const vcfUrl = settings.CardDAV.addressBookUrl + `${vcard.uid}.vcf`;
|
|
||||||
const res = await fetch(vcfUrl, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: vcard.raw,
|
|
||||||
headers: {
|
|
||||||
...options.headers,
|
|
||||||
['Authorization']: this.getAuthHeader(),
|
|
||||||
['Content-Type']: 'text/vcard; charset=utf-8'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`Push failed: ${res.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkConnectivity = async (settings: CarddavSettingsInterface): Promise<AppHttpResponse> => {
|
const checkConnectivity = async (settings: CarddavSettingsInterface): Promise<AppHttpResponse> => {
|
||||||
|
|
@ -54,24 +33,194 @@ export function carddavGenericAdapter(): AdapterInterface {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getList = async (): Promise<VCardMeta[]> => {
|
const isVcfNode = (response: Element): boolean => {
|
||||||
return Promise.resolve([]);
|
const href = response.querySelector('href')?.textContent;
|
||||||
|
const successPropstat = Array.from(response.querySelectorAll('propstat')).find(
|
||||||
|
propstat => propstat.querySelector('status')?.textContent?.includes('200 OK')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!successPropstat || !href || !href.endsWith('.vcf')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getMetaByUid = async (uid: string): Promise<VCardMeta|undefined> => {
|
||||||
|
const settings = getSettings();
|
||||||
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<D:prop>
|
||||||
|
<D:getetag />
|
||||||
|
<D:getlastmodified />
|
||||||
|
<D:resourcetype />
|
||||||
|
<C:address-data>
|
||||||
|
<C:prop name="UID"/>
|
||||||
|
<C:prop name="FN"/>
|
||||||
|
</C:address-data>
|
||||||
|
</D:prop>
|
||||||
|
<C:filter>
|
||||||
|
<C:prop-filter name="UID">
|
||||||
|
<C:text-match match-type="contains">${uid}</C:text-match>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:filter>
|
||||||
|
</C:addressbook-query>`;
|
||||||
|
|
||||||
|
const res = await PlatformHttpClient.request({
|
||||||
|
url: settings.CardDAV.addressBookUrl,
|
||||||
|
method: 'REPORT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(settings),
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'Depth': '1'
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xml = parser.parseFromString(res.data, 'application/xml');
|
||||||
|
const responses = xml.querySelectorAll('response');
|
||||||
|
|
||||||
|
if(!responses || responses.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = responses[0];
|
||||||
|
const href = response.querySelector('href')?.textContent || '';
|
||||||
|
const etag = response.querySelector('getetag')?.textContent || '';
|
||||||
|
const lastModified = response.querySelector('getlastmodified')?.textContent || '';
|
||||||
|
const adressData = response.querySelector('address-data')?.textContent || '';
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
href,
|
||||||
|
etag: etag?.replace(/"/g, '') || '', // Remove quotes from etag
|
||||||
|
lastModified: lastModified ? new Date(lastModified) : new Date(),
|
||||||
|
uid: uidOutOfString(adressData),
|
||||||
|
fn: fnOutOfString(adressData)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
href,
|
||||||
|
etag: etag?.replace(/"/g, '') || '', // Remove quotes from etag
|
||||||
|
lastModified: lastModified ? new Date(lastModified) : new Date(),
|
||||||
|
uid: uidOutOfString(adressData),
|
||||||
|
fn: fnOutOfString(adressData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pull = async (uid: string): Promise<VCardRaw | undefined> => {
|
|
||||||
return Promise.resolve(undefined);
|
const getMetaList = async (): Promise<VCardMeta[]> => {
|
||||||
|
const settings = getSettings();
|
||||||
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<D:prop>
|
||||||
|
<D:getetag />
|
||||||
|
<D:getlastmodified />
|
||||||
|
<D:resourcetype />
|
||||||
|
<C:address-data>
|
||||||
|
<C:prop name="UID"/>
|
||||||
|
<C:prop name="FN"/>
|
||||||
|
</C:address-data>
|
||||||
|
</D:prop>
|
||||||
|
</C:addressbook-query>`;
|
||||||
|
|
||||||
|
const res = await PlatformHttpClient.request({
|
||||||
|
url: settings.CardDAV.addressBookUrl ,
|
||||||
|
method: 'REPORT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(settings),
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'Depth': '1'
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xml = parser.parseFromString(res.data, 'application/xml');
|
||||||
|
|
||||||
|
const responses = xml.querySelectorAll('response');
|
||||||
|
const vcardMetas: VCardMeta[] = [];
|
||||||
|
|
||||||
|
responses.forEach(response => {
|
||||||
|
if(!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVcfNode(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = response.querySelector('href')?.textContent;
|
||||||
|
const etag = response.querySelector('getetag')?.textContent;
|
||||||
|
const lastModified = response.querySelector('getlastmodified')?.textContent;
|
||||||
|
const adressData = response.querySelector('address-data')?.textContent || '';
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vcardMetas.push({
|
||||||
|
href,
|
||||||
|
etag: etag?.replace(/"/g, '') || '', // Remove quotes from etag
|
||||||
|
lastModified: lastModified ? new Date(lastModified) : new Date(),
|
||||||
|
uid: uidOutOfString(adressData),
|
||||||
|
fn: fnOutOfString(adressData)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return vcardMetas;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const pull = async (href: string): Promise<VCardRaw | undefined> => {
|
||||||
|
const settings = getSettings();
|
||||||
|
const vcfUrl = new URL(href, settings.CardDAV.addressBookUrl).toString();
|
||||||
|
console.log('vcfUrl', vcfUrl);
|
||||||
|
const res = await PlatformHttpClient.request({
|
||||||
|
url: vcfUrl,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(settings)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log({
|
||||||
|
uid: uidOutOfString(res.data),
|
||||||
|
raw: res.data,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
uid: uidOutOfString(res.data),
|
||||||
|
raw: res.data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const push = async (vcard: VCardRaw): Promise<void> => {
|
const push = async (vcard: VCardRaw): Promise<void> => {
|
||||||
return Promise.resolve(undefined);
|
const settings = getSettings();
|
||||||
|
|
||||||
|
const vcfUrl = settings.CardDAV.addressBookUrl + `/${vcard.uid}.vcf`;
|
||||||
|
const res = await PlatformHttpClient.request({
|
||||||
|
url: vcfUrl,
|
||||||
|
method: 'PUT',
|
||||||
|
body: vcard.raw,
|
||||||
|
headers: {
|
||||||
|
['Authorization']: getAuthHeader(settings),
|
||||||
|
['Content-Type']: 'text/vcard; charset=utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('done push', res);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
checkConnectivity,
|
checkConnectivity,
|
||||||
getList,
|
getMetaByUid,
|
||||||
|
getMetaList,
|
||||||
pull,
|
pull,
|
||||||
push,
|
push
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
6
src/sync/adapters/index.ts
Normal file
6
src/sync/adapters/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { carddavGenericAdapter } from "src/sync/adapters/carddavGeneric";
|
||||||
|
|
||||||
|
export const adapters = {
|
||||||
|
None: undefined,
|
||||||
|
CardDAV: carddavGenericAdapter()
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,17 @@
|
||||||
|
import { getList, getMetaByUID, singlePull, } from "src/sync/singlePull";
|
||||||
|
import { singlePush } from "src/sync/singlePush";
|
||||||
|
|
||||||
export const sync = {
|
export const sync = {
|
||||||
|
singlePull,
|
||||||
|
singlePush,
|
||||||
|
getList,
|
||||||
|
do: async () => {
|
||||||
|
const list = await getList();
|
||||||
|
if (list && list[0]) {
|
||||||
|
getMetaByUID('019730a76c153-4458-a488-2f227fab60e7')
|
||||||
|
// singlePull(list[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
running: {},
|
||||||
|
enabled:{},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
26
src/sync/singlePull.ts
Normal file
26
src/sync/singlePull.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { getSettings } from "src/context/sharedSettingsContext";
|
||||||
|
import { adapters } from "src/sync/adapters";
|
||||||
|
|
||||||
|
export async function getList() {
|
||||||
|
const setting = getSettings();
|
||||||
|
return adapters[setting.syncSelected]?.getMetaList();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetaByUID(uid: string): Promise<void> {
|
||||||
|
const setting = getSettings();
|
||||||
|
const addapter = adapters[setting.syncSelected];
|
||||||
|
if(addapter) {
|
||||||
|
const res = addapter.getMetaByUid(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function singlePull(href: string): Promise<void> {
|
||||||
|
const setting = getSettings();
|
||||||
|
const addapter = adapters[setting.syncSelected];
|
||||||
|
|
||||||
|
if(addapter) {
|
||||||
|
const res = addapter.pull(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/sync/singlePush.ts
Normal file
24
src/sync/singlePush.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { TFile } from "obsidian";
|
||||||
|
import { getFrontmatterFromFiles } from "src/contacts";
|
||||||
|
import { vcard } from "src/contacts/vcard";
|
||||||
|
import { getSettings } from "src/context/sharedSettingsContext";
|
||||||
|
import { adapters } from "src/sync/adapters";
|
||||||
|
|
||||||
|
export async function singlePush(file: TFile) {
|
||||||
|
const setting = getSettings();
|
||||||
|
const frontmatter = (await getFrontmatterFromFiles([file]))[0]
|
||||||
|
const addapter = adapters[setting.syncSelected];
|
||||||
|
|
||||||
|
if(addapter) {
|
||||||
|
const result = await vcard.toString([file]);
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await addapter.push({
|
||||||
|
uid: frontmatter.data['UID'].split(':').pop(),
|
||||||
|
raw: result.vcards
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import * as React from "react";
|
||||||
import { Contact, parseKey } from "src/contacts";
|
import { Contact, parseKey } from "src/contacts";
|
||||||
import { getApp } from "src/context/sharedAppContext";
|
import { getApp } from "src/context/sharedAppContext";
|
||||||
import { fileId, openFile } from "src/file/file";
|
import { fileId, openFile } from "src/file/file";
|
||||||
|
import { sync } from "src/sync";
|
||||||
import Avatar from "src/ui/sidebar/components/Avatar";
|
import Avatar from "src/ui/sidebar/components/Avatar";
|
||||||
import { CopyableItem } from "src/ui/sidebar/components/CopyableItem";
|
import { CopyableItem } from "src/ui/sidebar/components/CopyableItem";
|
||||||
|
|
||||||
|
|
@ -243,6 +244,20 @@ export const ContactView = (props: ContactProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
data-icon="refresh-ccw-dot"
|
||||||
|
className={
|
||||||
|
"clickable-icon nav-action-button "
|
||||||
|
}
|
||||||
|
aria-label="Sync"
|
||||||
|
ref={(element) => (buttons.current[2] = element)}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
sync.do();
|
||||||
|
// sync.singlePush(contact.file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export interface AppHttpRequest {
|
export interface AppHttpRequest {
|
||||||
url: string;
|
url: string;
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'PROPFIND' | 'REPORT';
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
body?: any;
|
body?: any;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/util/vcard.ts
Normal file
36
src/util/vcard.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export function uidOutOfString(messyString: string):string|undefined {
|
||||||
|
const unfolded = messyString.replace(/\r\n[ \t]/g, '');
|
||||||
|
const match = unfolded.match(/^uid:.*$/gim);
|
||||||
|
let uid;
|
||||||
|
if(match) {
|
||||||
|
const uidLine = match[0]
|
||||||
|
const parts = uidLine.split(':');
|
||||||
|
uid = parts.length > 1 ? parts.at(-1)!.trim() : '';
|
||||||
|
}
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fnOutOfString(messyString: string):string|undefined {
|
||||||
|
const unfolded = messyString.replace(/\r\n[ \t]/g, '');
|
||||||
|
const match = unfolded.match(/^fn:.*$/gim);
|
||||||
|
let fn;
|
||||||
|
if(match) {
|
||||||
|
const uidLine = match[0]
|
||||||
|
const parts = uidLine.split(':');
|
||||||
|
fn = parts.length > 1 ? parts.at(-1)!.trim() : '';
|
||||||
|
}
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Zero dependency uuid generator as its not used for millions of records
|
||||||
|
export function generateUUID(): string {
|
||||||
|
const timestamp = Date.now().toString(16).padStart(12, '0');
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
}).replace(/^(.{24})/, (_, p1) => {
|
||||||
|
return timestamp + p1.slice(timestamp.length);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue