fix: better handling of many posts

This commit is contained in:
2026-02-10 22:48:13 +01:00
parent 7e4457c15d
commit 6bbf13dd41
10 changed files with 285 additions and 24 deletions

View File

@@ -60,6 +60,17 @@ export interface PostFilter {
month?: number;
}
export interface PaginatedResult<T> {
items: T[];
hasMore: boolean;
total: number;
}
export interface PaginationOptions {
limit?: number;
offset?: number;
}
export class PostEngine extends EventEmitter {
private currentProjectId: string = 'default';
@@ -455,7 +466,49 @@ export class PostEngine extends EventEmitter {
return this.dbRowToPostData(dbPost, '');
}
async getAllPosts(): Promise<PostData[]> {
async getAllPosts(options?: PaginationOptions): Promise<PaginatedResult<PostData>> {
const db = getDatabase().getLocal();
const limit = options?.limit ?? 500;
const offset = options?.offset ?? 0;
// Get total count for hasMore calculation
const countResult = await db
.select({ count: posts.id })
.from(posts)
.where(eq(posts.projectId, this.currentProjectId))
.all();
const total = countResult.length;
const dbPosts = await db
.select()
.from(posts)
.where(eq(posts.projectId, this.currentProjectId))
.orderBy(desc(posts.createdAt))
.limit(limit)
.offset(offset)
.all();
const items: PostData[] = [];
for (const dbPost of dbPosts) {
const postData = await this.getPost(dbPost.id);
if (postData) {
items.push(postData);
}
}
return {
items,
hasMore: offset + items.length < total,
total,
};
}
/**
* Internal method to get all posts without pagination.
* Used by methods that need to iterate over all posts (search, tags, categories, etc.)
*/
private async getAllPostsUnpaginated(): Promise<PostData[]> {
const db = getDatabase().getLocal();
const dbPosts = await db
.select()
@@ -574,7 +627,7 @@ export class PostEngine extends EventEmitter {
args: [query],
});
const projectPosts = await this.getAllPosts();
const projectPosts = await this.getAllPostsUnpaginated();
const projectPostIds = new Set(projectPosts.map(p => p.id));
return result.rows
@@ -594,7 +647,7 @@ export class PostEngine extends EventEmitter {
}
async getAvailableTags(): Promise<string[]> {
const allPosts = await this.getAllPosts();
const allPosts = await this.getAllPostsUnpaginated();
const tags = new Set<string>();
for (const post of allPosts) {
for (const tag of post.tags) {
@@ -605,7 +658,7 @@ export class PostEngine extends EventEmitter {
}
async getAvailableCategories(): Promise<string[]> {
const allPosts = await this.getAllPosts();
const allPosts = await this.getAllPostsUnpaginated();
const categories = new Set<string>();
for (const post of allPosts) {
for (const cat of post.categories) {
@@ -616,7 +669,7 @@ export class PostEngine extends EventEmitter {
}
async getPostsByYearMonth(): Promise<{ year: number; month: number; count: number }[]> {
const allPosts = await this.getAllPosts();
const allPosts = await this.getAllPostsUnpaginated();
const counts = new Map<string, { year: number; month: number; count: number }>();
for (const post of allPosts) {

View File

@@ -1,5 +1,5 @@
export { TaskManager, taskManager, type Task, type TaskProgress, type TaskStatus } from './TaskManager';
export { PostEngine, getPostEngine, type PostData, type PostFilter, type SearchResult } from './PostEngine';
export { PostEngine, getPostEngine, type PostData, type PostFilter, type SearchResult, type PaginatedResult, type PaginationOptions } from './PostEngine';
export { MediaEngine, getMediaEngine, type MediaData } from './MediaEngine';
export { SyncEngine, getSyncEngine, type SyncConfig, type SyncResult, type SyncDirection, type SyncStatus } from './SyncEngine';
export { ProjectEngine, getProjectEngine, type ProjectData } from './ProjectEngine';

View File

@@ -1,6 +1,6 @@
import { ipcMain, dialog, shell } from 'electron';
import { eq } from 'drizzle-orm';
import { getPostEngine, PostData, PostFilter } from '../engine/PostEngine';
import { getPostEngine, PostData, PostFilter, PaginationOptions } from '../engine/PostEngine';
import { getMediaEngine, MediaData } from '../engine/MediaEngine';
import { getSyncEngine, SyncConfig, SyncDirection } from '../engine/SyncEngine';
import { getDropboxSyncEngine, DropboxSyncConfig, ConflictResolution } from '../engine/DropboxSyncEngine';
@@ -99,9 +99,9 @@ export function registerIpcHandlers(): void {
return engine.getPost(id);
});
ipcMain.handle('posts:getAll', async () => {
ipcMain.handle('posts:getAll', async (_, options?: PaginationOptions) => {
const engine = getPostEngine();
return engine.getAllPosts();
return engine.getAllPosts(options);
});
ipcMain.handle('posts:getByStatus', async (_, status: 'draft' | 'published' | 'archived') => {
@@ -231,6 +231,18 @@ export function registerIpcHandlers(): void {
return engine.getMedia(id);
});
ipcMain.handle('media:getUrl', async (_, id: string) => {
// Returns the bds-media:// protocol URL for a media item
return `bds-media://${id}`;
});
ipcMain.handle('media:getFilePath', async (_, id: string) => {
// Returns the actual file path for a media item (for debugging/advanced use)
const db = getDatabase().getLocal();
const mediaItem = await db.select().from(media).where(eq(media.id, id)).get();
return mediaItem?.filePath ?? null;
});
ipcMain.handle('media:getAll', async () => {
const engine = getMediaEngine();
return engine.getAllMedia();

View File

@@ -1,14 +1,29 @@
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, ipcMain } from 'electron';
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, ipcMain, protocol, net } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import { getDatabase } from './database';
import { registerIpcHandlers } from './ipc';
import { media } from './database/schema';
import { eq } from 'drizzle-orm';
let mainWindow: BrowserWindow | null = null;
// Check if dev server is likely running (only in development)
const isDev = process.env.NODE_ENV === 'development';
// Register custom protocol scheme as privileged (must be done before app is ready)
protocol.registerSchemesAsPrivileged([
{
scheme: 'bds-media',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
},
},
]);
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1400,
@@ -298,6 +313,53 @@ async function initialize(): Promise<void> {
const db = getDatabase();
await db.initializeLocal();
// Register custom protocol for serving media files
// URLs like bds-media://media-id will be resolved to the actual file
protocol.handle('bds-media', async (request) => {
try {
const url = new URL(request.url);
const mediaIdentifier = url.hostname; // bds-media://media-id or bds-media://filename.jpg
const database = getDatabase().getLocal();
// First, try to find by ID (most common case)
let mediaItem = await database
.select()
.from(media)
.where(eq(media.id, mediaIdentifier))
.get();
// If not found by ID, try by filename
if (!mediaItem) {
mediaItem = await database
.select()
.from(media)
.where(eq(media.filename, mediaIdentifier))
.get();
}
// If still not found, try by original name
if (!mediaItem) {
mediaItem = await database
.select()
.from(media)
.where(eq(media.originalName, mediaIdentifier))
.get();
}
if (mediaItem && mediaItem.filePath) {
// Use net.fetch to get the file - this handles the file protocol properly
return net.fetch(`file://${mediaItem.filePath}`);
}
// Return a 404 response if media not found
return new Response('Media not found', { status: 404 });
} catch (error) {
console.error('Error serving media:', error);
return new Response('Internal server error', { status: 500 });
}
});
// Register IPC handlers
registerIpcHandlers();
}

View File

@@ -20,7 +20,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
update: (id: string, data: unknown) => ipcRenderer.invoke('posts:update', id, data),
delete: (id: string) => ipcRenderer.invoke('posts:delete', id),
get: (id: string) => ipcRenderer.invoke('posts:get', id),
getAll: () => ipcRenderer.invoke('posts:getAll'),
getAll: (options?: { limit?: number; offset?: number }) => ipcRenderer.invoke('posts:getAll', options),
getByStatus: (status: string) => ipcRenderer.invoke('posts:getByStatus', status),
publish: (id: string) => ipcRenderer.invoke('posts:publish', id),
unpublish: (id: string) => ipcRenderer.invoke('posts:unpublish', id),
@@ -46,6 +46,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
update: (id: string, data: unknown) => ipcRenderer.invoke('media:update', id, data),
delete: (id: string) => ipcRenderer.invoke('media:delete', id),
get: (id: string) => ipcRenderer.invoke('media:get', id),
getUrl: (id: string) => ipcRenderer.invoke('media:getUrl', id),
getFilePath: (id: string) => ipcRenderer.invoke('media:getFilePath', id),
getAll: () => ipcRenderer.invoke('media:getAll'),
rebuildFromFiles: () => ipcRenderer.invoke('media:rebuildFromFiles'),
getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => ipcRenderer.invoke('media:getThumbnail', id, size),