Skip to content
Draft
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
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@
"@mantine/hooks": "^7.7.0",
"@tanstack/react-router": "^1.29.2",
"@tauri-apps/api": "^1",
"@types/sql.js": "^1.4.9",
"bits-ui": "^0.14.0",
"clsx": "^2.1.0",
"mantine-datatable": "^7.6.1",
"react": "18.2",
"react-dom": "18.2",
"sql.js": "^1.10.3",
"tailwind-merge": "^2.2.0",
"tailwind-variants": "^0.1.20",
"tauri-settings": "^0.3.4"
Expand Down
21 changes: 18 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { safeAsyncEventHandler } from "$lib/async";
import { LibraryProvider } from "$lib/contexts/library";
import { LocalCalibreLibraryProvider, WebCalibreLibraryProvider } from "$lib/contexts/library";
import { SettingsProvider } from "$lib/contexts/settings";
import { theme } from "$lib/theme";
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
Expand All @@ -21,6 +21,9 @@ declare module "@tanstack/react-router" {

export const App = () => {
const [libraryPath, setLibraryPath] = useState<string | null>(null);
const [libraryFSDirectoryHandle, setLibraryFSDirectoryHandle] = useState<FileSystemDirectoryHandle | null>(
null
);
const updateLibraryPath = useCallback(async () => {
const libPath = await settings.get("calibreLibraryPath");
setLibraryPath(libPath);
Expand All @@ -39,19 +42,31 @@ export const App = () => {
onLibraryPathPicked={() => {
safeAsyncEventHandler(updateLibraryPath)();
}}
onLibraryFSDirectoryHandlePicked={setLibraryFSDirectoryHandle}
/>
</MantineProvider>
);
}

if (libraryFSDirectoryHandle) {
return <SettingsProvider value={settings}>
<WebCalibreLibraryProvider directoryHandle={libraryFSDirectoryHandle}>
<ColorSchemeScript defaultColorScheme="auto" />
<MantineProvider theme={theme} defaultColorScheme="auto">
<RouterProvider router={router} />
</MantineProvider>
</WebCalibreLibraryProvider>
</SettingsProvider>;
}
Comment on lines +51 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Logic flaw: web provider renders when libraryFSDirectoryHandle exists but libraryPath is null, bypassing the first-time setup check. This could lead to inconsistent state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/App.tsx
Line: 51:60

Comment:
**logic:** Logic flaw: web provider renders when `libraryFSDirectoryHandle` exists but `libraryPath` is null, bypassing the first-time setup check. This could lead to inconsistent state.

How can I resolve this? If you propose a fix, please make it concise.


return (
<SettingsProvider value={settings}>
<LibraryProvider libraryPath={libraryPath}>
<LocalCalibreLibraryProvider libraryPath={libraryPath}>
<ColorSchemeScript defaultColorScheme="auto" />
<MantineProvider theme={theme} defaultColorScheme="auto">
<RouterProvider router={router} />
</MantineProvider>
</LibraryProvider>
</LocalCalibreLibraryProvider>
</SettingsProvider>
);
};
40 changes: 31 additions & 9 deletions src/components/pages/firstTimeSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { commands } from "@/bindings";
import { safeAsyncEventHandler } from "@/lib/async";
import { isDesktop } from "@/lib/platform";
import { pickLibrary } from "@/lib/services/library";
import { createLibrary } from "@/lib/services/library/_internal/pickLibrary";
import { settings } from "@/stores/settings";
Expand All @@ -23,7 +24,11 @@ const openFilePicker = async (): Promise<

export const FirstTimeSetup = ({
onLibraryPathPicked,
}: { onLibraryPathPicked: () => void }) => {
onLibraryFSDirectoryHandlePicked,
}: {
onLibraryPathPicked: () => void;
onLibraryFSDirectoryHandlePicked: (handle: FileSystemDirectoryHandle) => void;
}) => {
return (
<Stack align="center" justify="flex-start" h={"100vh"} p="sm">
<Title>Welcome to Citadel!</Title>
Expand All @@ -34,16 +39,33 @@ export const FirstTimeSetup = ({
</Text>
<Button
onPointerDown={safeAsyncEventHandler(async () => {
const returnStatus = await openFilePicker();
if (returnStatus.type === "invalid library path selected") {
return;
}
if (isDesktop()) {
const returnStatus = await openFilePicker();
if (returnStatus.type === "invalid library path selected") {
return;
}

if (returnStatus.type === "new library selected") {
await createLibrary(returnStatus.path);
}
await settings.set("calibreLibraryPath", returnStatus.path);
onLibraryPathPicked();
} else {
const currentDirHandle = await (
// This function is experimental, and not yet in the TS types.
window as typeof window & {
showDirectoryPicker: () => Promise<FileSystemDirectoryHandle>;
}
).showDirectoryPicker();
if (currentDirHandle === undefined) return;

if (returnStatus.type === "new library selected") {
await createLibrary(returnStatus.path);
await settings.set(
"calibreLibraryPath",
`webfilesystem://${currentDirHandle.name}`,
);
onLibraryFSDirectoryHandlePicked(currentDirHandle);
onLibraryPathPicked();
}
await settings.set("calibreLibraryPath", returnStatus.path);
onLibraryPathPicked();
})}
>
Choose Calibre library folder
Expand Down
2 changes: 1 addition & 1 deletion src/lib/contexts/library/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface LibraryProviderProps {
children: React.ReactNode;
libraryPath: string;
}
export const LibraryProvider = ({
export const LocalCalibreLibraryProvider = ({
children,
libraryPath,
}: LibraryProviderProps) => {
Expand Down
50 changes: 50 additions & 0 deletions src/lib/contexts/library/WebProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useReducer } from "react";
import { DEFAULT_CONTEXT_VALUE, LibraryContext, LibraryState } from "./context";
import { Options, initClient } from "@/lib/services/library";
import { reducer } from "./reducer";

const webLibraryFromHandle = (handle: FileSystemDirectoryHandle): Options => ({
libraryDirectoryHandle: handle,
libraryType: "calibre",
connectionType: "web",
});

interface LibraryProviderProps {
children: React.ReactNode;
directoryHandle: FileSystemDirectoryHandle;
}
export const WebCalibreLibraryProvider = ({
children,
directoryHandle,
}: LibraryProviderProps) => {
const [context, dispatch] = useReducer(reducer, DEFAULT_CONTEXT_VALUE);

useEffect(() => {
if (context.state === LibraryState.error) {
console.log(context.error);
}
}, [context]);

useEffect(() => {
initClient(webLibraryFromHandle(directoryHandle))
.then((client) => {
dispatch({
type: "init",
client,
});
})
.catch(() => {
dispatch({ type: "error", error: new Error("Failed to init Library") });
});
Comment on lines +36 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Generic error message makes debugging difficult - consider preserving the original error or adding more context

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/contexts/library/WebProvider.tsx
Line: 36:38

Comment:
**style:** Generic error message makes debugging difficult - consider preserving the original error or adding more context

How can I resolve this? If you propose a fix, please make it concise.


return () => {
dispatch({ type: "shutdown" });
};
}, [directoryHandle]);
Comment on lines +28 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Loading state is never set during initialization - consider dispatching a loading action before calling initClient

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/contexts/library/WebProvider.tsx
Line: 28:43

Comment:
**logic:** Loading state is never set during initialization - consider dispatching a loading action before calling `initClient`

How can I resolve this? If you propose a fix, please make it concise.


return (
<LibraryContext.Provider value={context}>
{!context.loading && children}
</LibraryContext.Provider>
);
};
5 changes: 3 additions & 2 deletions src/lib/contexts/library/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LibraryProvider } from "./Provider";
import { LocalCalibreLibraryProvider } from "./Provider";
import {WebCalibreLibraryProvider} from "./WebProvider";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing space after opening brace in import statement

Suggested change
import {WebCalibreLibraryProvider} from "./WebProvider";
import { WebCalibreLibraryProvider } from "./WebProvider";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/contexts/library/index.ts
Line: 2:2

Comment:
**style:** Missing space after opening brace in import statement

```suggestion
import { WebCalibreLibraryProvider } from "./WebProvider";
```

How can I resolve this? If you propose a fix, please make it concise.

import { LibraryState } from "./context";
import { useLibrary } from "./hooks";

export { LibraryProvider, useLibrary, LibraryState };
export { LocalCalibreLibraryProvider, WebCalibreLibraryProvider, useLibrary, LibraryState };
7 changes: 7 additions & 0 deletions src/lib/isDefined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const isDefined = <T>(value: T | undefined): value is T =>
value !== undefined;

export const isNonNull = <T>(value: T | null): value is T => value !== null;

export const isSomething = <T>(value: T | undefined | null): value is T =>
value !== undefined && value !== null;
3 changes: 3 additions & 0 deletions src/lib/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isDesktop = () => {
return "__TAURI__" in window && window.__TAURI__;
};
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing error handling for environments where window is undefined (e.g., SSR or Node.js contexts)

Suggested change
export const isDesktop = () => {
return "__TAURI__" in window && window.__TAURI__;
};
export const isDesktop = () => {
return typeof window !== "undefined" && "__TAURI__" in window && window.__TAURI__;
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/platform.ts
Line: 1:3

Comment:
**logic:** Missing error handling for environments where `window` is undefined (e.g., SSR or Node.js contexts)

```suggestion
export const isDesktop = () => {
  return typeof window !== "undefined" && "__TAURI__" in window && window.__TAURI__;
};
```

How can I resolve this? If you propose a fix, please make it concise.

6 changes: 5 additions & 1 deletion src/lib/services/library/_internal/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ export interface RemoteConnectionOptions {
connectionType: "remote";
url: string;
}
export interface WebConnectionOptions {
connectionType: "web";
libraryDirectoryHandle: FileSystemDirectoryHandle;
}

export type Options = {
libraryType: "calibre";
} & (LocalConnectionOptions | RemoteConnectionOptions);
} & (LocalConnectionOptions | RemoteConnectionOptions | WebConnectionOptions);
5 changes: 5 additions & 0 deletions src/lib/services/library/_internal/adapters/calibre.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import {
commands,
type ImportableFile,
Expand All @@ -11,6 +12,7 @@ import type {
Options,
RemoteConnectionOptions,
} from "../_types";
import { genWebCalibreClient } from "./web";

const genLocalCalibreClient = async (
options: LocalConnectionOptions,
Expand Down Expand Up @@ -176,6 +178,9 @@ export const initCalibreClient = async (options: Options): Promise<Library> => {
if (options.connectionType === "remote") {
return genRemoteCalibreClient(options);
}
if (options.connectionType === "web") {
return genWebCalibreClient(options);
}

return genLocalCalibreClient(options);
};
149 changes: 149 additions & 0 deletions src/lib/services/library/_internal/adapters/web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import initSqlJs from "sql.js";

import { LibraryBook } from "@/bindings";

import { Library, WebConnectionOptions } from "../_types";
import { loadDb } from "./web/db";
import {
createAuthorRepository,
createBookAuthorLinkRepository,
createBookRepository,
} from "./web/repositories/sqljs";
import { createCatalogService } from "./web/services/catalog";
import { createAuthorService } from "./web/services/author";
import { libraryAuthor } from "./web/entities/LibraryAuthor";

export const genWebCalibreClient = async (
options: WebConnectionOptions,
): Promise<Library> => {
const SQL = await initSqlJs({
// TODO: Now Citadel requires being online on the web? ... that is probably fine, yes.
locateFile: (file: string) => `https://sql.js.org/dist/${file}`,
});
const bookRepo = createBookRepository(options.libraryDirectoryHandle, SQL);
const bookAuthorLinkRepo = createBookAuthorLinkRepository(
options.libraryDirectoryHandle,
SQL,
);
const authorRepo = createAuthorRepository(
options.libraryDirectoryHandle,
SQL,
);
const authorService = createAuthorService(authorRepo);
const catalogService = createCatalogService(
bookRepo,
bookAuthorLinkRepo,
authorRepo,
);

return {
listBooks: async () => {
const db = await loadDb(options.libraryDirectoryHandle, SQL);
if (!db) return [];

const res = db.exec("SELECT * FROM 'books'");
if (res.length === 0) {
return [];
}
const rows = res[0].values;

const coverImagePromises = rows.map((row) => {
const path = (row[9] ?? "").toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Magic number row[9] for accessing cover path column is brittle and unclear. Consider using named constants or a mapping function.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web.ts
Line: 51:51

Comment:
**style:** Magic number `row[9]` for accessing cover path column is brittle and unclear. Consider using named constants or a mapping function.

How can I resolve this? If you propose a fix, please make it concise.

if (path === "") {
return null;
}
return coverImageFromPath(path, options.libraryDirectoryHandle);
});
const coverImages = await Promise.all(coverImagePromises);

const addCoverImageToBook = (
book: LibraryBook,
coverImage: File | null,
): LibraryBook => {
if (coverImage === null) {
return book;
}
return {
...book,
cover_image: {
kind: "Remote",
url: URL.createObjectURL(coverImage),
local_path: null,
},
};
};
const books = (await catalogService.all()).map((book, index) =>
addCoverImageToBook(book, coverImages[index]),
);

return books;
},
listAuthors: async () => {
return (await authorService.all()).map(libraryAuthor.fromAuthor);
},
listValidFileTypes: () => {
return Promise.resolve([]);
},
getCoverPathForBook: () => {
throw new Error("Not implemented");
},
getCoverUrlForBook: () => {
throw new Error("Not implemented");
},
getDefaultFilePathForBook: () => {
throw new Error("Not implemented");
},
getImportableFileMetadata: () => {
throw new Error("Not implemented");
},
sendToDevice: () => {
throw new Error("Not implemented");
},
updateBook: () => {
throw new Error("Not implemented");
},
addImportableFileByMetadata: () => {
throw new Error("Not implemented");
},
checkFileImportable: () => {
throw new Error("Not implemented");
},
};
};

const coverImageFromPath = async (
path: string,
libraryDirectoryHandle: FileSystemDirectoryHandle,
): Promise<File | null> => {
const parts = path.split("/");
const maxDepth = parts.length;
let currDepth = 0;
let handle: FileSystemDirectoryHandle | FileSystemFileHandle | null =
libraryDirectoryHandle;

while (currDepth < maxDepth) {
try {
// File names can be invalid, which throws an error.
handle = await handle.getDirectoryHandle(parts[currDepth]);
} catch (e) {
console.log(e, handle, parts[currDepth], parts, currDepth);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Console.log in production code should be replaced with proper error handling or logging system.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web.ts
Line: 129:129

Comment:
**style:** Console.log in production code should be replaced with proper error handling or logging system.

How can I resolve this? If you propose a fix, please make it concise.

handle = null;
break;
}
if (handle === null) {
break;
}
currDepth++;
}

if (handle === null) {
return null;
}
try {
// Getting the cover image can throw errors
const fileHandle = await handle.getFileHandle("cover.jpg");
return fileHandle.getFile();
} catch (e) {
return null;
}
};
Empty file.
Loading