Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/components/NovelCover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ function isFromDB(
return 'chaptersDownloaded' in item;
}

/**
* Render a novel cover (grid or list) with badges, title, image, and selection handling.
*
* @param item - Novel item data (cover, name, and optional DB-specific chapter counts).
* @param onPress - Callback invoked when the cover is pressed (used when not in selection mode).
* @param libraryStatus - Whether the novel is present in the user's library (shows InLibrary badge and alters cover opacity).
* @param theme - Theme colors used for badges, ripples, and title styling.
* @param isSelected - Whether the novel is currently selected (affects visual highlighting).
* @param addSkeletonLoading - When true and `item.completeRow` is present, render skeleton loading for that row; otherwise render nothing for that row.
* @param inActivity - When true, show a small activity badge on the cover.
* @param onLongPress - Callback invoked to trigger selection behavior; receives the novel item.
* @param hasSelection - Optional explicit flag to enable selection mode; if omitted, selection mode is inferred from `selectedNovelIds`.
* @param globalSearch - When true, use the global-search layout and sizing (three-column grid behaviour).
* @param selectedNovelIds - Optional array of selected novel IDs used to infer selection mode when `hasSelection` is not provided.
* @param imageRequestInit - Optional image request init (headers, etc.); a User-Agent header is injected if none provided.
* @returns A React element that displays the novel cover in the appropriate layout (grid or list) or a skeleton loading component when applicable.
*/
function NovelCover<
TNovel extends CoverItemLibrary | CoverItemPlugin | CoverItemDB,
>({
Expand Down Expand Up @@ -462,4 +479,4 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
paddingTop: 2,
},
});
});
17 changes: 16 additions & 1 deletion src/database/manager/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ interface ExecutableSelect<TResult = any> {

let _dbManager: DbManager;

/**
* Produces a SQL expression that casts the given value to INTEGER.
*
* @param value - A literal value, string, or SQL column/expression to cast to integer
* @returns A SQL fragment representing `CAST(value AS INTEGER)`
*/
export function castInt(value: number | string | AnyColumn) {
return sql`CAST(${value} AS INTEGER)`;
}
Expand Down Expand Up @@ -132,6 +138,15 @@ export const createDbManager = (dbInstance: DrizzleDb) => {
type TableNames = GetSelectTableName<Schema[keyof Schema]>;
type FireOn = Array<{ table: TableNames; ids?: number[] }>;

/**
* Subscribes to a query and returns its current result set, updating when the database signals relevant changes.
*
* Initializes state with a synchronous execution of `query`, then registers a reactive subscription that updates the returned rows when records for the specified tables/IDs change.
*
* @param query - An executable select query whose results will be observed.
* @param fireOn - Array of triggers specifying table names and optional ID lists that should cause the query to refresh when modified.
* @returns The current rows produced by `query.all()`; updates automatically when matching database changes occur.
*/
export function useLiveQuery<T extends ExecutableSelect>(
query: T,
fireOn: FireOn,
Expand Down Expand Up @@ -160,4 +175,4 @@ export function useLiveQuery<T extends ExecutableSelect>(
}, [sqlString, paramsKey, fireOnKey]);

return data;
}
}
18 changes: 13 additions & 5 deletions src/database/queries/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import { createTestDb, cleanupTestDb, type TestDb } from './testDb';
let mockTestDbInstance: TestDb | null = null;

/**
* Sets up the test database
* This should be called in beforeEach of test files
* Initialize and return the in-memory test database for queries.
*
* If a test database is already initialized, it is first cleaned up and replaced.
*
* @returns The initialized test database instance
*/
export function setupTestDatabase(): TestDb {
if (mockTestDbInstance) {
Expand All @@ -27,7 +30,10 @@ export function setupTestDatabase(): TestDb {
}

/**
* Gets the current test database instance
* Retrieves the active test database instance for the current test run.
*
* @returns The initialized TestDb instance.
* @throws Error if the test database has not been initialized; call setupTestDatabase() first.
*/
export function getTestDb(): TestDb {
if (!mockTestDbInstance) {
Expand All @@ -39,7 +45,9 @@ export function getTestDb(): TestDb {
}

/**
* Cleans up the test database
* Dispose of the current in-memory test database and clear its module reference.
*
* If a test database has been initialized, calls the cleanup routine and sets the internal instance to `null`.
*/
export function teardownTestDatabase() {
if (mockTestDbInstance) {
Expand Down Expand Up @@ -152,4 +160,4 @@ jest.setTimeout(10000);
// The database is automatically cleaned up in afterAll
afterAll(() => {
teardownTestDatabase();
});
});
48 changes: 40 additions & 8 deletions src/database/queries/__tests__/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
} from '@database/schema';

/**
* Clears all tables in the test database
* Remove all rows from test tables used by the application.
*
* Specifically deletes entries from NovelCategory, Chapter, Novel, and Repository, and deletes Category rows whose `id` is greater than 2 (preserving `id` 1 and 2).
*/
export function clearAllTables(testDb: TestDb) {
const { sqlite } = testDb;
Expand All @@ -31,7 +33,11 @@ export function clearAllTables(testDb: TestDb) {
}

/**
* Inserts a test novel into the database
* Create and insert a test novel record into the database.
*
* @param testDb - Test database context used for the insertion
* @param data - Optional fields to override the default novel properties
* @returns The id of the inserted novel
*/
export async function insertTestNovel(
testDb: TestDb,
Expand Down Expand Up @@ -70,7 +76,11 @@ export async function insertTestNovel(
}

/**
* Inserts a test chapter into the database
* Create and insert a chapter record linked to a novel for tests.
*
* @param novelId - The id of the novel to associate the chapter with
* @param data - Optional partial chapter fields to override default test values
* @returns The inserted chapter's id
*/
export async function insertTestChapter(
testDb: TestDb,
Expand Down Expand Up @@ -106,7 +116,10 @@ export async function insertTestChapter(
}

/**
* Inserts a test category into the database
* Inserts a category row for tests, using sensible defaults for missing fields.
*
* @param data - Optional overrides for the category fields; if `name` is omitted a timestamped default is used
* @returns The id of the newly inserted category
*/
export async function insertTestCategory(
testDb: TestDb,
Expand All @@ -128,7 +141,11 @@ export async function insertTestCategory(
}

/**
* Inserts a test repository into the database
* Create and insert a repository record for tests.
*
* If `data.url` is not provided, a unique test URL is generated.
*
* @returns The inserted repository's id
*/
export async function insertTestRepository(
testDb: TestDb,
Expand All @@ -149,7 +166,9 @@ export async function insertTestRepository(
}

/**
* Inserts a novel-category association
* Creates an association between a novel and a category in the test database.
*
* @returns The id of the created novel-category association
*/
export async function insertTestNovelCategory(
testDb: TestDb,
Expand All @@ -172,7 +191,11 @@ export async function insertTestNovelCategory(
}

/**
* Inserts a novel with optional chapters
* Create a test novel and optionally add chapters linked to it.
*
* @param novelData - Partial fields to override on the created novel
* @param chapters - Array of partial chapter records to insert for the novel; each entry becomes a chapter linked to the created novel
* @returns An object containing the inserted novel's `novelId` and an array of inserted `chapterIds`
*/
export async function insertTestNovelWithChapters(
testDb: TestDb,
Expand Down Expand Up @@ -201,6 +224,15 @@ export interface TestFixtures {
novelCategories?: Array<{ novelId: number; categoryId: number }>;
}

/**
* Inserts provided test fixtures into the test database and returns the created record IDs.
*
* Processes fixtures in this order when present: novels, chapters, categories, repositories, and novel-category associations.
*
* @param testDb - Test database context used for performing inserts
* @param fixtures - Collections of fixture objects to insert; fields can override default test values
* @returns An object containing arrays of inserted IDs: `novelIds`, `chapterIds`, `categoryIds`, and `repositoryIds`
*/
export async function seedTestData(
testDb: TestDb,
fixtures: TestFixtures,
Expand Down Expand Up @@ -256,4 +288,4 @@ export async function seedTestData(
}

return { novelIds, chapterIds, categoryIds, repositoryIds };
}
}
19 changes: 13 additions & 6 deletions src/database/queries/__tests__/testDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ const MIGRATION_STATEMENTS = [
];

/**
* Creates a fresh in-memory SQLite database with schema and migrations
* @returns Database instance and dbManager
* Create a fresh in-memory SQLite database preloaded with schema, migrations, triggers, and default categories for use in tests.
*
* The database is configured with testing-friendly PRAGMAs, migration SQL is applied, required tables are verified (including `Novel`), production-like triggers are created, and default Category rows are seeded.
*
* @returns An object with `sqlite` (the better-sqlite3 Database), `drizzleDb` (the Drizzle ORM instance bound to the database), and `dbManager` (a test-friendly database manager)
*/
export function createTestDb() {
// Create in-memory database
Expand Down Expand Up @@ -149,15 +152,19 @@ export function createTestDb() {
}

/**
* Closes and cleans up a test database
* Close the underlying SQLite connection for a test database.
*
* @param testDb - The test database object returned by `createTestDb`; its underlying SQLite connection will be closed
*/
export function cleanupTestDb(testDb: ReturnType<typeof createTestDb>) {
testDb.sqlite.close();
}

/**
* Gets a dbManager instance for a test database
* Convenience function for tests
* Retrieve the dbManager from a test database instance.
*
* @param testDb - The object returned by `createTestDb`
* @returns The `dbManager` associated with `testDb`
*/
export function getTestDbManager(testDb: ReturnType<typeof createTestDb>) {
return testDb.dbManager;
Expand All @@ -166,4 +173,4 @@ export function getTestDbManager(testDb: ReturnType<typeof createTestDb>) {
/**
* Type export for test database
*/
export type TestDb = ReturnType<typeof createTestDb>;
export type TestDb = ReturnType<typeof createTestDb>;
8 changes: 6 additions & 2 deletions src/database/queries/__tests__/testDbManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const wrapBuilder = <T extends object>(builder: T): T => {
};

/**
* Creates a test-compatible dbManager that works with better-sqlite3
* Create a test-compatible IDbManager that adapts a Drizzle ORM database and a better-sqlite3 Database.
*
* @param drizzleDb - The Drizzle ORM database instance to delegate query builders and async operations to
* @param sqlite - The better-sqlite3 Database instance used for synchronous test helpers
* @returns An IDbManager that delegates to `drizzleDb` and provides test-friendly methods (e.g., `getSync`, `allSync`, `batch`, `write`, `transaction`)
*/
export function createTestDbManager(
drizzleDb: DrizzleDb,
Expand Down Expand Up @@ -131,4 +135,4 @@ export function createTestDbManager(
};

return dbManager;
}
}
14 changes: 13 additions & 1 deletion src/database/utils/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@ import {
} from '@database/constants';
import { SQL, sql } from 'drizzle-orm';

/**
* Convert a chapter order key into a raw SQL ordering fragment.
*
* @param order - Ordering key that selects which chapter sort expression to use
* @returns A raw SQL fragment representing the corresponding ORDER BY expression; falls back to the default position-ascending expression if the key is not found
*/
export function chapterOrderToSQL(order: ChapterOrderKey) {
const o = CHAPTER_ORDER[order] ?? CHAPTER_ORDER.positionAsc;
return sql.raw(o);
}

/**
* Builds an SQL condition fragment from an array of chapter filter keys.
*
* @param filter - Optional array of chapter filter keys to combine with `AND`; if omitted or empty, no filtering is applied
* @returns An `SQL` fragment representing the combined `WHERE`-style condition for the provided filters; when `filter` is undefined or empty, returns an expression that evaluates to `true`
*/
export function chapterFilterToSQL(filter?: ChapterFilterKey[]) {
if (!filter || !filter.length) return sql.raw('true');
let filters: SQL | undefined;
Expand All @@ -22,4 +34,4 @@ export function chapterFilterToSQL(filter?: ChapterFilterKey[]) {
}
});
return filters ?? sql.raw('true');
}
}
9 changes: 7 additions & 2 deletions src/hooks/persisted/useNovel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,12 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => {
);

// #endregion
// #region setters
/**
* Produce the list of page identifiers for a novel, using totalPages if available or custom pages otherwise.
*
* @param tmpNovel - Novel metadata used to determine available pages
* @returns An array of page numbers as strings; always contains at least `'1'`
*/

async function calculatePages(tmpNovel: NovelInfo) {
let tmpPages: string[];
Expand Down Expand Up @@ -687,4 +692,4 @@ export const deleteCachedNovels = async () => {
}
_deleteCachedNovels();
};
// #endregion
// #endregion
9 changes: 8 additions & 1 deletion src/screens/novel/NovelContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const defaultValue = {} as NovelContextType;

const NovelContext = createContext<NovelContextType>(defaultValue);

/**
* Provides NovelContext to descendants by combining novel hook data with safe-area and orientation-derived layout metrics and a chapter text cache.
*
* @param children - Element subtree that will receive the context
* @param route - Navigation route for 'Novel' or 'Chapter'; used to derive the novel identifier (either `path` or a `NovelInfo` object) and `pluginId`
* @returns The NovelContext provider wrapping `children`
*/
export function NovelContextProvider({
children,

Expand Down Expand Up @@ -70,4 +77,4 @@ export function NovelContextProvider({
export const useNovelContext = () => {
const context = useContext(NovelContext);
return context;
};
};
13 changes: 12 additions & 1 deletion src/screens/reader/hooks/useChapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ import { useNovelContext } from '@screens/novel/NovelContext';

const emmiter = new NativeEventEmitter(NativeVolumeButtonListener);

/**
* Manages chapter loading, navigation, UI visibility, auto-scrolling, and reading progress for a reader WebView.
*
* Handles loading and caching chapter text, prefetching adjacent chapters/pages, volume-button and auto-scroll controls,
* tracking/marking progress, history updates, and immersive mode toggling.
*
* @param webViewRef - Ref to the WebView used to render and control the chapter content
* @param initialChapter - The initial ChapterInfo to display
* @param novel - Metadata for the current novel (used for fetching, caching, and pagination)
* @returns An object exposing reader state (hidden, chapter, nextChapter, prevChapter, error, loading, chapterText), setters (setHidden, setChapter, setLoading), and actions (saveProgress, hideHeader, navigateChapter, refetch, getChapter)
*/
export default function useChapter(
webViewRef: RefObject<WebView | null>,
initialChapter: ChapterInfo,
Expand Down Expand Up @@ -377,4 +388,4 @@ export default function useChapter(
getChapter,
],
);
}
}