fix: better handling of many posts
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user