feat: gallery macro
This commit is contained in:
@@ -182,6 +182,15 @@ export class DatabaseConnection {
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post_media (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
post_id TEXT NOT NULL,
|
||||
media_id TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_sync_status ON posts(sync_status);
|
||||
@@ -190,6 +199,9 @@ export class DatabaseConnection {
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_media_post ON post_media(post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_media_media ON post_media(media_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS post_media_post_media_idx ON post_media(post_id, media_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
|
||||
@@ -96,6 +96,19 @@ export const postLinks = sqliteTable('post_links', {
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
// Post-Media links - tracks which media files are linked to which posts
|
||||
export const postMedia = sqliteTable('post_media', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id').notNull(),
|
||||
postId: text('post_id').notNull(),
|
||||
mediaId: text('media_id').notNull(),
|
||||
sortOrder: integer('sort_order').notNull().default(0), // For ordering media within a post
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
}, (table) => ({
|
||||
// Composite unique index: a media can only be linked once to a post
|
||||
postMediaIdx: uniqueIndex('post_media_post_media_idx').on(table.postId, table.mediaId),
|
||||
}));
|
||||
|
||||
// Tags table - stores tag metadata with optional colors
|
||||
export const tags = sqliteTable('tags', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -143,6 +156,8 @@ export type Setting = typeof settings.$inferSelect;
|
||||
export type NewSetting = typeof settings.$inferInsert;
|
||||
export type PostLink = typeof postLinks.$inferSelect;
|
||||
export type NewPostLink = typeof postLinks.$inferInsert;
|
||||
export type PostMediaLink = typeof postMedia.$inferSelect;
|
||||
export type NewPostMediaLink = typeof postMedia.$inferInsert;
|
||||
export type Tag = typeof tags.$inferSelect;
|
||||
export type NewTag = typeof tags.$inferInsert;
|
||||
export type ChatConversation = typeof chatConversations.$inferSelect;
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface MediaData {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
tags: string[];
|
||||
linkedPostIds?: string[]; // Posts this media is linked to
|
||||
}
|
||||
|
||||
export interface MediaMetadata {
|
||||
@@ -45,6 +46,7 @@ export interface MediaMetadata {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tags: string[];
|
||||
linkedPostIds?: string[]; // Posts this media is linked to (persisted in sidecar)
|
||||
}
|
||||
|
||||
export class MediaEngine extends EventEmitter {
|
||||
@@ -227,6 +229,7 @@ export class MediaEngine extends EventEmitter {
|
||||
createdAt: mediaData.createdAt.toISOString(),
|
||||
updatedAt: mediaData.updatedAt.toISOString(),
|
||||
tags: mediaData.tags,
|
||||
linkedPostIds: mediaData.linkedPostIds,
|
||||
};
|
||||
|
||||
// Write YAML-like format consistent with posts
|
||||
@@ -246,6 +249,9 @@ export class MediaEngine extends EventEmitter {
|
||||
lines.push(`createdAt: ${metadata.createdAt}`);
|
||||
lines.push(`updatedAt: ${metadata.updatedAt}`);
|
||||
lines.push(`tags: [${metadata.tags.map(t => `"${t}"`).join(', ')}]`);
|
||||
if (metadata.linkedPostIds && metadata.linkedPostIds.length > 0) {
|
||||
lines.push(`linkedPostIds: [${metadata.linkedPostIds.map(id => `"${id}"`).join(', ')}]`);
|
||||
}
|
||||
lines.push('---');
|
||||
|
||||
await fs.writeFile(sidecarPath, lines.join('\n'), 'utf-8');
|
||||
@@ -267,6 +273,7 @@ export class MediaEngine extends EventEmitter {
|
||||
|
||||
const metadata: Partial<MediaMetadata> = {
|
||||
tags: [],
|
||||
linkedPostIds: [],
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -316,14 +323,24 @@ export class MediaEngine extends EventEmitter {
|
||||
break;
|
||||
case 'tags':
|
||||
// Parse array format: ["tag1", "tag2"]
|
||||
const match = value.match(/\[(.*)\]/);
|
||||
if (match) {
|
||||
metadata.tags = match[1]
|
||||
const tagsMatch = value.match(/\[(.*)\]/);
|
||||
if (tagsMatch) {
|
||||
metadata.tags = tagsMatch[1]
|
||||
.split(',')
|
||||
.map(t => t.trim().replace(/"/g, ''))
|
||||
.filter(t => t.length > 0);
|
||||
}
|
||||
break;
|
||||
case 'linkedPostIds':
|
||||
// Parse array format: ["postId1", "postId2"]
|
||||
const postIdsMatch = value.match(/\[(.*)\]/);
|
||||
if (postIdsMatch) {
|
||||
metadata.linkedPostIds = postIdsMatch[1]
|
||||
.split(',')
|
||||
.map(id => id.trim().replace(/"/g, ''))
|
||||
.filter(id => id.length > 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
295
src/main/engine/PostMediaEngine.ts
Normal file
295
src/main/engine/PostMediaEngine.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* PostMediaEngine
|
||||
*
|
||||
* Manages the relationship between posts and media files.
|
||||
* Handles linking, unlinking, ordering, and querying post-media associations.
|
||||
*
|
||||
* Data is persisted in two places:
|
||||
* 1. postMedia junction table in the database (for querying)
|
||||
* 2. linkedPostIds field in media sidecar files (source of truth for rebuild)
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { eq, and, asc } from 'drizzle-orm';
|
||||
import { getDatabase } from '../database';
|
||||
import { postMedia, PostMediaLink, NewPostMediaLink } from '../database/schema';
|
||||
import { getMediaEngine, MediaData } from './MediaEngine';
|
||||
|
||||
export interface PostMediaLinkData {
|
||||
id: string;
|
||||
projectId: string;
|
||||
postId: string;
|
||||
mediaId: string;
|
||||
sortOrder: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let postMediaEngineInstance: PostMediaEngine | null = null;
|
||||
|
||||
export class PostMediaEngine extends EventEmitter {
|
||||
private currentProjectId: string = 'default';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current project context
|
||||
*/
|
||||
setProjectContext(projectId: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
console.log(`[PostMediaEngine] setProjectContext: projectId=${projectId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a media file to a post
|
||||
*/
|
||||
async linkMediaToPost(postId: string, mediaId: string): Promise<PostMediaLinkData> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
// Get current highest sortOrder for this post
|
||||
const existingLinks = await this.getLinkedMediaForPost(postId);
|
||||
const maxSortOrder = existingLinks.length > 0
|
||||
? Math.max(...existingLinks.map(l => l.sortOrder))
|
||||
: -1;
|
||||
|
||||
const now = new Date();
|
||||
const link: NewPostMediaLink = {
|
||||
id: uuidv4(),
|
||||
projectId: this.currentProjectId,
|
||||
postId,
|
||||
mediaId,
|
||||
sortOrder: maxSortOrder + 1,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
await db.insert(postMedia).values(link);
|
||||
|
||||
// Update the media sidecar to include this post
|
||||
const media = await getMediaEngine().getMedia(mediaId);
|
||||
if (media) {
|
||||
const linkedPostIds = media.linkedPostIds || [];
|
||||
if (!linkedPostIds.includes(postId)) {
|
||||
await getMediaEngine().updateMedia(mediaId, {
|
||||
linkedPostIds: [...linkedPostIds, postId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const linkData: PostMediaLinkData = {
|
||||
id: link.id,
|
||||
projectId: link.projectId,
|
||||
postId: link.postId,
|
||||
mediaId: link.mediaId,
|
||||
sortOrder: link.sortOrder ?? 0,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
this.emit('mediaLinked', linkData);
|
||||
return linkData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a media file from a post
|
||||
*/
|
||||
async unlinkMediaFromPost(postId: string, mediaId: string): Promise<void> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
await db.delete(postMedia).where(
|
||||
and(
|
||||
eq(postMedia.postId, postId),
|
||||
eq(postMedia.mediaId, mediaId)
|
||||
)
|
||||
);
|
||||
|
||||
// Update the media sidecar to remove this post
|
||||
const media = await getMediaEngine().getMedia(mediaId);
|
||||
if (media) {
|
||||
const linkedPostIds = (media.linkedPostIds || []).filter(id => id !== postId);
|
||||
await getMediaEngine().updateMedia(mediaId, { linkedPostIds });
|
||||
}
|
||||
|
||||
this.emit('mediaUnlinked', { postId, mediaId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all media linked to a post, ordered by sortOrder
|
||||
*/
|
||||
async getLinkedMediaForPost(postId: string): Promise<PostMediaLinkData[]> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
const links = await db
|
||||
.select()
|
||||
.from(postMedia)
|
||||
.where(
|
||||
and(
|
||||
eq(postMedia.projectId, this.currentProjectId),
|
||||
eq(postMedia.postId, postId)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(postMedia.sortOrder));
|
||||
|
||||
return links.map(this.mapToLinkData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all posts linked to a media file
|
||||
*/
|
||||
async getLinkedPostsForMedia(mediaId: string): Promise<PostMediaLinkData[]> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
const links = await db
|
||||
.select()
|
||||
.from(postMedia)
|
||||
.where(
|
||||
and(
|
||||
eq(postMedia.projectId, this.currentProjectId),
|
||||
eq(postMedia.mediaId, mediaId)
|
||||
)
|
||||
);
|
||||
|
||||
return links.map(this.mapToLinkData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder media within a post
|
||||
* @param postId The post ID
|
||||
* @param mediaIds Array of media IDs in the new desired order
|
||||
*/
|
||||
async reorderMediaForPost(postId: string, mediaIds: string[]): Promise<void> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
// Update each media's sortOrder based on its position in the array
|
||||
for (let i = 0; i < mediaIds.length; i++) {
|
||||
await db.update(postMedia)
|
||||
.set({ sortOrder: i })
|
||||
.where(
|
||||
and(
|
||||
eq(postMedia.postId, postId),
|
||||
eq(postMedia.mediaId, mediaIds[i])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.emit('mediaReordered', { postId, mediaIds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the junction table from media sidecar files.
|
||||
* This is called during rebuild operations when the database needs to be
|
||||
* reconstructed from filesystem source of truth.
|
||||
*/
|
||||
async rebuildFromSidecars(): Promise<void> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
console.log('[PostMediaEngine] Rebuilding post-media links from sidecars...');
|
||||
|
||||
// Clear existing links for this project
|
||||
await db.delete(postMedia).where(eq(postMedia.projectId, this.currentProjectId));
|
||||
|
||||
// Get all media with their linkedPostIds
|
||||
const allMedia = await getMediaEngine().getAllMedia();
|
||||
|
||||
let linksCreated = 0;
|
||||
for (const media of allMedia) {
|
||||
const linkedPostIds = media.linkedPostIds || [];
|
||||
|
||||
for (let i = 0; i < linkedPostIds.length; i++) {
|
||||
const postId = linkedPostIds[i];
|
||||
const linkId = uuidv4();
|
||||
|
||||
await db.insert(postMedia).values({
|
||||
id: linkId,
|
||||
projectId: this.currentProjectId,
|
||||
postId,
|
||||
mediaId: media.id,
|
||||
sortOrder: i, // Preserve order from sidecar
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
linksCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PostMediaEngine] Rebuilt ${linksCreated} post-media links`);
|
||||
this.emit('rebuilt', { linksCreated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Import media from a file path and link it to a post.
|
||||
* This is a convenience method that combines import + link.
|
||||
*/
|
||||
async importMediaForPost(postId: string, sourcePath: string): Promise<PostMediaLinkData> {
|
||||
// Import the media file
|
||||
const importedMedia = await getMediaEngine().importMedia(sourcePath);
|
||||
|
||||
// Link it to the post
|
||||
return this.linkMediaToPost(postId, importedMedia.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get linked media with full media data
|
||||
*/
|
||||
async getLinkedMediaDataForPost(postId: string): Promise<Array<PostMediaLinkData & { media: MediaData }>> {
|
||||
const links = await this.getLinkedMediaForPost(postId);
|
||||
|
||||
const result: Array<PostMediaLinkData & { media: MediaData }> = [];
|
||||
for (const link of links) {
|
||||
const media = await getMediaEngine().getMedia(link.mediaId);
|
||||
if (media) {
|
||||
result.push({ ...link, media });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a media is linked to a post
|
||||
*/
|
||||
async isMediaLinkedToPost(postId: string, mediaId: string): Promise<boolean> {
|
||||
const db = getDatabase().getLocal();
|
||||
|
||||
const link = await db
|
||||
.select()
|
||||
.from(postMedia)
|
||||
.where(
|
||||
and(
|
||||
eq(postMedia.postId, postId),
|
||||
eq(postMedia.mediaId, mediaId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return link.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map database row to PostMediaLinkData
|
||||
*/
|
||||
private mapToLinkData(row: PostMediaLink): PostMediaLinkData {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.projectId,
|
||||
postId: row.postId,
|
||||
mediaId: row.mediaId,
|
||||
sortOrder: row.sortOrder,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton PostMediaEngine instance
|
||||
*/
|
||||
export function getPostMediaEngine(): PostMediaEngine {
|
||||
if (!postMediaEngineInstance) {
|
||||
postMediaEngineInstance = new PostMediaEngine();
|
||||
}
|
||||
return postMediaEngineInstance;
|
||||
}
|
||||
|
||||
// Export singleton for convenience
|
||||
export const postMediaEngine = getPostMediaEngine();
|
||||
@@ -1,6 +1,7 @@
|
||||
export { TaskManager, taskManager, type Task, type TaskProgress, type TaskStatus } from './TaskManager';
|
||||
export { PostEngine, getPostEngine, type PostData, type PostFilter, type SearchResult, type PaginatedResult, type PaginationOptions } from './PostEngine';
|
||||
export { MediaEngine, getMediaEngine, type MediaData } from './MediaEngine';
|
||||
export { PostMediaEngine, getPostMediaEngine, postMediaEngine, type PostMediaLinkData } from './PostMediaEngine';
|
||||
export { SyncEngine, getSyncEngine, type SyncConfig, type SyncResult, type SyncDirection, type SyncStatus } from './SyncEngine';
|
||||
export { ProjectEngine, getProjectEngine, type ProjectData } from './ProjectEngine';
|
||||
export { MetaEngine, getMetaEngine, type ProjectMetadata, DEFAULT_CATEGORIES } from './MetaEngine';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getDropboxSyncEngine, DropboxSyncConfig, ConflictResolution } from '../
|
||||
import { getProjectEngine, ProjectData } from '../engine/ProjectEngine';
|
||||
import { getMetaEngine } from '../engine/MetaEngine';
|
||||
import { getTagEngine } from '../engine/TagEngine';
|
||||
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
||||
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
||||
import { getDatabase } from '../database';
|
||||
import { media } from '../database/schema';
|
||||
@@ -77,6 +78,8 @@ export function registerIpcHandlers(): void {
|
||||
mediaEngine.setProjectContext(project.id, dataDir, internalDir);
|
||||
metaEngine.setProjectContext(project.id);
|
||||
tagEngine.setProjectContext(project.id);
|
||||
const postMediaEngine = getPostMediaEngine();
|
||||
postMediaEngine.setProjectContext(project.id);
|
||||
|
||||
// Sync meta on startup
|
||||
await metaEngine.syncOnStartup();
|
||||
@@ -101,6 +104,8 @@ export function registerIpcHandlers(): void {
|
||||
mediaEngine.setProjectContext(project.id, dataDir, internalDir);
|
||||
metaEngine.setProjectContext(project.id);
|
||||
tagEngine.setProjectContext(project.id);
|
||||
const postMediaEngine = getPostMediaEngine();
|
||||
postMediaEngine.setProjectContext(project.id);
|
||||
|
||||
// Sync meta on project switch
|
||||
await metaEngine.syncOnStartup();
|
||||
@@ -668,6 +673,53 @@ export function registerIpcHandlers(): void {
|
||||
return engine.syncTagsFromPosts();
|
||||
});
|
||||
|
||||
// ============ Post-Media Link Handlers ============
|
||||
|
||||
safeHandle('postMedia:link', async (_, postId: string, mediaId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.linkMediaToPost(postId, mediaId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:unlink', async (_, postId: string, mediaId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.unlinkMediaFromPost(postId, mediaId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:getForPost', async (_, postId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.getLinkedMediaForPost(postId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:getForMedia', async (_, mediaId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.getLinkedPostsForMedia(mediaId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:getMediaDataForPost', async (_, postId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.getLinkedMediaDataForPost(postId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:reorder', async (_, postId: string, mediaIds: string[]) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.reorderMediaForPost(postId, mediaIds);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:isLinked', async (_, postId: string, mediaId: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.isMediaLinkedToPost(postId, mediaId);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:import', async (_, postId: string, filePath: string) => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.importMediaForPost(postId, filePath);
|
||||
});
|
||||
|
||||
safeHandle('postMedia:rebuild', async () => {
|
||||
const engine = getPostMediaEngine();
|
||||
return engine.rebuildFromSidecars();
|
||||
});
|
||||
|
||||
// ============ Event Forwarding ============
|
||||
|
||||
// Forward engine events to renderer
|
||||
@@ -677,6 +729,7 @@ export function registerIpcHandlers(): void {
|
||||
const projectEngine = getProjectEngine();
|
||||
const metaEngine = getMetaEngine();
|
||||
const tagEngine = getTagEngine();
|
||||
const postMediaEngine = getPostMediaEngine();
|
||||
|
||||
const forwardEvent = (eventName: string) => {
|
||||
return (...args: unknown[]) => {
|
||||
@@ -713,6 +766,11 @@ export function registerIpcHandlers(): void {
|
||||
tagEngine.on('tagsMerged', forwardEvent('tags:merged'));
|
||||
tagEngine.on('tagsSynced', forwardEvent('tags:synced'));
|
||||
|
||||
postMediaEngine.on('mediaLinked', forwardEvent('postMedia:linked'));
|
||||
postMediaEngine.on('mediaUnlinked', forwardEvent('postMedia:unlinked'));
|
||||
postMediaEngine.on('mediaReordered', forwardEvent('postMedia:reordered'));
|
||||
postMediaEngine.on('rebuilt', forwardEvent('postMedia:rebuilt'));
|
||||
|
||||
syncEngine.on('syncStarted', forwardEvent('sync:started'));
|
||||
syncEngine.on('syncCompleted', forwardEvent('sync:completed'));
|
||||
syncEngine.on('syncFailed', forwardEvent('sync:failed'));
|
||||
|
||||
@@ -59,6 +59,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'),
|
||||
},
|
||||
|
||||
// Post-Media Links
|
||||
postMedia: {
|
||||
link: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:link', postId, mediaId),
|
||||
unlink: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:unlink', postId, mediaId),
|
||||
getForPost: (postId: string) => ipcRenderer.invoke('postMedia:getForPost', postId),
|
||||
getForMedia: (mediaId: string) => ipcRenderer.invoke('postMedia:getForMedia', mediaId),
|
||||
getMediaDataForPost: (postId: string) => ipcRenderer.invoke('postMedia:getMediaDataForPost', postId),
|
||||
reorder: (postId: string, mediaIds: string[]) => ipcRenderer.invoke('postMedia:reorder', postId, mediaIds),
|
||||
isLinked: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:isLinked', postId, mediaId),
|
||||
import: (postId: string, filePath: string) => ipcRenderer.invoke('postMedia:import', postId, filePath),
|
||||
rebuild: () => ipcRenderer.invoke('postMedia:rebuild'),
|
||||
},
|
||||
|
||||
// Sync
|
||||
sync: {
|
||||
configure: (config: unknown) => ipcRenderer.invoke('sync:configure', config),
|
||||
|
||||
Reference in New Issue
Block a user