diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index 45cd2be8..4c6b8aeb 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -14,6 +14,7 @@ export const METADATA_EXTRACTOR_SUPPORTED_EXTENSIONS = [ "application/x-cbz", "application/epub+zip", "application/zip", + "application/x-zip-compressed", "application/x-rar" ] export const COVER_ALLOWED_EXT = [".jpg", ".jpeg", ".png"] diff --git a/packages/web/src/books/states.ts b/packages/web/src/books/states.ts index 8dcf5a15..d1b6d0db 100644 --- a/packages/web/src/books/states.ts +++ b/packages/web/src/books/states.ts @@ -3,23 +3,21 @@ import { useProtectedTagIds, useTagsByIds } from "../tags/helpers" import { getLinkState, useLink, useLinks } from "../links/states" import { getBookDownloadsState, - booksDownloadStateSignal, - DownloadState + booksDownloadStateSignal } from "../download/states" import { useCollections } from "../collections/useCollections" -import { from, map, switchMap } from "rxjs" +import { map, switchMap } from "rxjs" import { plugin as localPlugin } from "../plugins/local" import { latestDatabase$ } from "../rxdb/useCreateDatabase" -import { isDefined, useForeverQuery, useQuery, useSignalValue } from "reactjrx" +import { useForeverQuery, useSignalValue } from "reactjrx" import { keyBy } from "lodash" import { Database } from "../rxdb" import { BookDocType, CollectionDocType } from "@oboku/shared" import { DeepReadonlyObject, MangoQuery } from "rxdb" -import { useVisibleBooks } from "./useVisibleBooks" -import { DeepReadonlyArray, RxDocument } from "rxdb/dist/types/types" +import { DeepReadonlyArray } from "rxdb/dist/types/types" import { useMemo } from "react" -import { getBooksQueryObj } from "./dbHelpers" -import { CollectionDocMethods } from "../rxdb/collections/collection" +import { observeBooks } from "./dbHelpers" +import { libraryStateSignal } from "../library/states" export const getBooksByIds = async (database: Database) => { const result = await database.collections.book.find({}).exec() @@ -33,13 +31,17 @@ export const getBooksByIds = async (database: Database) => { export const useBooks = ({ queryObj = {}, isNotInterested, - ids + ids, + includeProtected: _includeProtected }: { queryObj?: MangoQuery isNotInterested?: "none" | "with" | "only" ids?: DeepReadonlyArray + includeProtected?: boolean } = {}) => { const serializedIds = JSON.stringify(ids) + const { isLibraryUnlocked } = useSignalValue(libraryStateSignal) + const includeProtected = _includeProtected || isLibraryUnlocked return useForeverQuery({ queryKey: [ @@ -47,17 +49,22 @@ export const useBooks = ({ "get", "many", "books", - { isNotInterested, serializedIds }, + { isNotInterested, serializedIds, includeProtected }, queryObj ], - queryFn: () => { - const finalQueryObj = getBooksQueryObj({ queryObj, isNotInterested, ids }) - - return latestDatabase$.pipe( - switchMap((db) => db.collections.book.find(finalQueryObj).$), + queryFn: () => + latestDatabase$.pipe( + switchMap((db) => + observeBooks({ + db, + includeProtected, + ids, + isNotInterested, + queryObj + }) + ), map((items) => items.map((item) => item.toJSON())) ) - } }) } @@ -268,23 +275,6 @@ export const useEnrichedBookState = ({ ) } -/** - * @deprecated - */ -export const useDownloadedBookWithUnsafeProtectedIdsState = () => { - const downloadState = useSignalValue(booksDownloadStateSignal) - const { data: books } = useBooks() - - return useMemo( - () => - books?.filter( - (book) => - downloadState[book._id]?.downloadState === DownloadState.Downloaded - ), - [downloadState, books] - ) -} - /** * @deprecated */ @@ -296,7 +286,11 @@ export const useBooksAsArrayState = ({ > }) => { const { data: books = {}, isPending } = useBooksDic() - const visibleBookIds = useVisibleBookIds() ?? [] + const { data: visibleBooks } = useBooks() + const visibleBookIds = useMemo( + () => visibleBooks?.map((item) => item._id) ?? [], + [visibleBooks] + ) const bookResult: (BookQueryResult & { downloadState: ReturnType @@ -325,27 +319,6 @@ export const useBooksAsArrayState = ({ } } -export const useVisibleBookIds = ( - params: Parameters[0] = {} -) => { - return useVisibleBooks(params).data?.map((book) => book._id) -} - -/** - * @deprecated - */ -export const useBookTagsState = ({ - bookId, - tags = {} -}: { - bookId: string - tags: ReturnType["data"] -}) => { - const { data: book } = useBook({ id: bookId }) - - return book?.tags?.map((id) => tags[id]).filter(isDefined) -} - /** * @deprecated */ @@ -365,4 +338,3 @@ export const useBookLinksState = ({ export const books$ = latestDatabase$.pipe( switchMap((database) => database?.book.find({}).$) ) - diff --git a/packages/web/src/books/useVisibleBooks.ts b/packages/web/src/books/useVisibleBooks.ts deleted file mode 100644 index e8e3b9d2..00000000 --- a/packages/web/src/books/useVisibleBooks.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useBooks } from "./states" -import { useProtectedTagIds } from "../tags/helpers" -import { useSignalValue } from "reactjrx" -import { libraryStateSignal } from "../library/states" -import { useMemo } from "react" -import { intersection } from "lodash" - -export const useVisibleBooks = ( - params: Parameters[0] = {} -) => { - const { data: books, isLoading: isBooksLoading } = useBooks(params) - const { data: protectedTagIds, isLoading: isTagsLoading } = - useProtectedTagIds() - const { isLibraryUnlocked } = useSignalValue(libraryStateSignal) - - const data = useMemo(() => { - if (isLibraryUnlocked) { - return books - } else { - return books?.filter( - (book) => intersection(protectedTagIds, book?.tags || []).length === 0 - ) - } - }, [books, protectedTagIds, isLibraryUnlocked]) - - return { data, isLoading: isBooksLoading || isTagsLoading } -} diff --git a/packages/web/src/collections/CollectionDetailsScreen.tsx b/packages/web/src/collections/CollectionDetailsScreen.tsx index 1d8c86dd..166670f0 100644 --- a/packages/web/src/collections/CollectionDetailsScreen.tsx +++ b/packages/web/src/collections/CollectionDetailsScreen.tsx @@ -4,7 +4,6 @@ import { useNavigate, useParams } from "react-router-dom" import EmptyLibraryAsset from "../assets/empty-library.svg" import CollectionBgSvg from "../assets/series-bg.svg" import { BookListWithControls } from "../books/bookList/BookListWithControls" -import { useVisibleBookIds } from "../books/states" import { signal, useSignalValue } from "reactjrx" import { ListActionSorting, @@ -13,6 +12,8 @@ import { import { useCollectionActionsDrawer } from "./CollectionActionsDrawer/useCollectionActionsDrawer" import { useCollection } from "./useCollection" import { COLLECTION_EMPTY_ID } from "../constants.shared" +import { useMemo } from "react" +import { useBooks } from "../books/states" type ScreenParams = { id: string @@ -40,10 +41,15 @@ export const CollectionDetailsScreen = () => { id }) - const visibleBooks = useVisibleBookIds({ + const { data: visibleBooks } = useBooks({ ids: collection?.books ?? [] }) + const visibleBookIds = useMemo( + () => visibleBooks?.map((item) => item._id) ?? [], + [visibleBooks] + ) + const { open: openActionDrawer } = useCollectionActionsDrawer( id, (changes) => { @@ -110,7 +116,7 @@ export const CollectionDetailsScreen = () => { { diff --git a/packages/web/src/collections/useCollections.ts b/packages/web/src/collections/useCollections.ts index ad056419..f24301d2 100644 --- a/packages/web/src/collections/useCollections.ts +++ b/packages/web/src/collections/useCollections.ts @@ -2,7 +2,7 @@ import { CollectionDocType, directives, ReadingStateState } from "@oboku/shared" import { useLocalSettings } from "../settings/states" import { useForeverQuery, useSignalValue } from "reactjrx" import { latestDatabase$ } from "../rxdb/useCreateDatabase" -import { map, switchMap } from "rxjs" +import { map, switchMap, tap } from "rxjs" import { MangoQuery } from "rxdb" import { getMetadataFromCollection } from "./getMetadataFromCollection" import { DeepReadonlyArray } from "rxdb/dist/types/types" @@ -18,6 +18,7 @@ export const useCollections = ({ ids, isNotInterested, readingState = "any", + includeProtected: _includeProtected, ...options }: { queryObj?: MangoQuery @@ -31,12 +32,14 @@ export const useCollections = ({ isNotInterested?: "with" | "none" | "only" | undefined readingState?: "ongoing" | "finished" | "any" ids?: DeepReadonlyArray + includeProtected?: boolean } = {}) => { const { hideDirectivesFromCollectionName } = useLocalSettings() const serializedBookIds = JSON.stringify(bookIds) const serializedIds = JSON.stringify(ids) const { isLibraryUnlocked } = useSignalValue(libraryStateSignal) const { showCollectionWithProtectedContent } = useLocalSettings() + const includeProtected = _includeProtected || isLibraryUnlocked return useForeverQuery({ queryKey: [ @@ -47,7 +50,7 @@ export const useCollections = ({ serializedBookIds, serializedIds, showCollectionWithProtectedContent, - isLibraryUnlocked, + includeProtected, hideDirectivesFromCollectionName, isNotInterested }, @@ -64,11 +67,10 @@ export const useCollections = ({ */ observeBooks({ db, - includeProtected: isLibraryUnlocked - // isNotInterested + includeProtected }).pipe( switchMap((books) => { - const protectedBookIds = books.map(({ _id }) => _id) + const visibleBooks = books.map(({ _id }) => _id) const notInterestedBookIds = books .filter(({ isNotInterested }) => !!isNotInterested) .map(({ _id }) => _id) @@ -99,7 +101,7 @@ export const useCollections = ({ map((collections) => collections.filter((collection) => { if ( - isLibraryUnlocked || + includeProtected || collection.books.length === 0 || showCollectionWithProtectedContent === "hasNormalContent" ) @@ -111,7 +113,7 @@ export const useCollections = ({ */ const extraBooksFromCollection = difference( collection.books, - protectedBookIds + visibleBooks ) const hasSuspiciousExtraBook = diff --git a/packages/web/src/download/useDownloadedBooks.ts b/packages/web/src/download/useDownloadedBooks.ts new file mode 100644 index 00000000..a37c7b1b --- /dev/null +++ b/packages/web/src/download/useDownloadedBooks.ts @@ -0,0 +1,18 @@ +import { useMemo } from "react" +import { useSignalValue } from "reactjrx" +import { useBooks } from "../books/states" +import { booksDownloadStateSignal, DownloadState } from "./states" + +export const useDownloadedBooks = () => { + const downloadState = useSignalValue(booksDownloadStateSignal) + const { data: books } = useBooks() + + return useMemo( + () => + books?.filter( + (book) => + downloadState[book._id]?.downloadState === DownloadState.Downloaded + ), + [downloadState, books] + ) +} diff --git a/packages/web/src/home/helpers.ts b/packages/web/src/home/helpers.ts index cec30004..aa93f548 100644 --- a/packages/web/src/home/helpers.ts +++ b/packages/web/src/home/helpers.ts @@ -2,12 +2,12 @@ import { useMemo } from "react" import { ReadingStateState } from "@oboku/shared" import { useBooksSortedBy } from "../books/helpers" import { useProtectedTagIds } from "../tags/helpers" -import { useVisibleBooks } from "../books/useVisibleBooks" +import { useBooks } from "../books/states" export const useContinueReadingBooks = () => { const { isPending } = useProtectedTagIds() - const { data: booksAsArray, isLoading: isBooksPending } = useVisibleBooks({ + const { data: booksAsArray, isLoading: isBooksPending } = useBooks({ isNotInterested: "none" }) const booksSortedByDate = useBooksSortedBy(booksAsArray, "activity") @@ -28,7 +28,7 @@ export const useContinueReadingBooks = () => { } export const useRecentlyAddedBooks = () => { - const { data: booksAsArray } = useVisibleBooks({ + const { data: booksAsArray } = useBooks({ isNotInterested: "none" }) diff --git a/packages/web/src/library/collections/CollectionStateDialog.tsx b/packages/web/src/library/shelves/CollectionStateDialog.tsx similarity index 100% rename from packages/web/src/library/collections/CollectionStateDialog.tsx rename to packages/web/src/library/shelves/CollectionStateDialog.tsx diff --git a/packages/web/src/library/collections/FilterBar.tsx b/packages/web/src/library/shelves/FilterBar.tsx similarity index 100% rename from packages/web/src/library/collections/FilterBar.tsx rename to packages/web/src/library/shelves/FilterBar.tsx diff --git a/packages/web/src/library/collections/FiltersDrawer.tsx b/packages/web/src/library/shelves/FiltersDrawer.tsx similarity index 100% rename from packages/web/src/library/collections/FiltersDrawer.tsx rename to packages/web/src/library/shelves/FiltersDrawer.tsx diff --git a/packages/web/src/library/collections/LibraryCollectionScreen.tsx b/packages/web/src/library/shelves/LibraryCollectionScreen.tsx similarity index 97% rename from packages/web/src/library/collections/LibraryCollectionScreen.tsx rename to packages/web/src/library/shelves/LibraryCollectionScreen.tsx index 4686429d..8219b93b 100644 --- a/packages/web/src/library/collections/LibraryCollectionScreen.tsx +++ b/packages/web/src/library/shelves/LibraryCollectionScreen.tsx @@ -15,7 +15,7 @@ import { useMeasureElement } from "../../common/utils" import { CollectionList } from "../../collections/list/CollectionList" import { useDebouncedCallback } from "use-debounce" import { signal, useSignalValue } from "reactjrx" -import { useShelve } from "./useShelve" +import { useShelves } from "./useShelves" import { FilterBar } from "./FilterBar" import { useCreateCollection } from "../../collections/useCreateCollection" import { collectionsListSignal } from "./state" @@ -48,7 +48,7 @@ export const LibraryCollectionScreen = () => { collectionsListSignal, ({ viewMode }) => ({ viewMode }) ) - const { data: collections = [] } = useShelve() + const { data: collections = [] } = useShelves() const onScroll = useDebouncedCallback((value: Scroll) => { libraryCollectionScreenPreviousScrollState.setValue(value) diff --git a/packages/web/src/library/collections/state.ts b/packages/web/src/library/shelves/state.ts similarity index 100% rename from packages/web/src/library/collections/state.ts rename to packages/web/src/library/shelves/state.ts diff --git a/packages/web/src/library/collections/useShelve.ts b/packages/web/src/library/shelves/useShelves.ts similarity index 97% rename from packages/web/src/library/collections/useShelve.ts rename to packages/web/src/library/shelves/useShelves.ts index 63ae97e6..9db8749f 100644 --- a/packages/web/src/library/collections/useShelve.ts +++ b/packages/web/src/library/shelves/useShelves.ts @@ -6,7 +6,7 @@ import { collectionsListSignal } from "./state" import { useCollection } from "../../collections/useCollection" import { COLLECTION_EMPTY_ID } from "../../constants.shared" -export const useShelve = () => { +export const useShelves = () => { const { showNotInterestedCollections } = useSignalValue( libraryStateSignal, ({ showNotInterestedCollections }) => ({ showNotInterestedCollections }) diff --git a/packages/web/src/navigation/AppNavigator.tsx b/packages/web/src/navigation/AppNavigator.tsx index 9e920415..90edf1ca 100644 --- a/packages/web/src/navigation/AppNavigator.tsx +++ b/packages/web/src/navigation/AppNavigator.tsx @@ -25,7 +25,7 @@ import { StatisticsScreen } from "../settings/StatisticsScreen" import { BackToReadingDialog } from "../reading/BackToReadingDialog" import { ProblemsScreen } from "../problems/ProblemsScreen" import { LibraryBooksScreen } from "../library/LibraryBooksScreen" -import { LibraryCollectionScreen } from "../library/collections/LibraryCollectionScreen" +import { LibraryCollectionScreen } from "../library/shelves/LibraryCollectionScreen" import { LibraryTagsScreen } from "../library/LibraryTagsScreen" import { memo, useEffect, useRef } from "react" import { UnlockLibraryDialog } from "../auth/UnlockLibraryDialog" diff --git a/packages/web/src/problems/BookDanglingCollections.tsx b/packages/web/src/problems/BookDanglingCollections.tsx new file mode 100644 index 00000000..3bae21d2 --- /dev/null +++ b/packages/web/src/problems/BookDanglingCollections.tsx @@ -0,0 +1,29 @@ +import { LinkOffRounded } from "@mui/icons-material" +import { ListItemButton, ListItemIcon, ListItemText } from "@mui/material" +import { BookDocType } from "@oboku/shared" +import { DeepReadonlyObject } from "rxdb" +import { getMetadataFromBook } from "../books/metadata" + +export const BookDanglingCollections = ({ + danglingBooks, + doc, + onClick +}: { + doc: DeepReadonlyObject + danglingBooks: string[] + onClick?: () => void +}) => { + return ( + + + + + + + ) +} diff --git a/packages/web/src/problems/BookDanglingLinks.tsx b/packages/web/src/problems/BookDanglingLinks.tsx new file mode 100644 index 00000000..2076794d --- /dev/null +++ b/packages/web/src/problems/BookDanglingLinks.tsx @@ -0,0 +1,29 @@ +import { LinkOffRounded } from "@mui/icons-material" +import { ListItemButton, ListItemIcon, ListItemText } from "@mui/material" +import { BookDocType } from "@oboku/shared" +import { DeepReadonlyObject } from "rxdb" +import { getMetadataFromBook } from "../books/metadata" + +export const BookDanglingLinks = ({ + danglingBooks, + doc, + onClick +}: { + doc: DeepReadonlyObject + danglingBooks: string[] + onClick?: () => void +}) => { + return ( + + + + + + + ) +} diff --git a/packages/web/src/problems/CollectionDanglingBooks.tsx b/packages/web/src/problems/CollectionDanglingBooks.tsx new file mode 100644 index 00000000..bcf747af --- /dev/null +++ b/packages/web/src/problems/CollectionDanglingBooks.tsx @@ -0,0 +1,28 @@ +import { LinkOffRounded } from "@mui/icons-material" +import { ListItemButton, ListItemIcon, ListItemText } from "@mui/material" +import { CollectionDocType } from "@oboku/shared" +import { getMetadataFromCollection } from "../collections/getMetadataFromCollection" + +export const CollectionDanglingBooks = ({ + danglingBooks, + doc, + onClick +}: { + doc: CollectionDocType + danglingBooks: string[] + onClick?: () => void +}) => { + return ( + + + + + + + ) +} diff --git a/packages/web/src/problems/ProblemsScreen.tsx b/packages/web/src/problems/ProblemsScreen.tsx index 4ce14580..a45b61c4 100644 --- a/packages/web/src/problems/ProblemsScreen.tsx +++ b/packages/web/src/problems/ProblemsScreen.tsx @@ -1,14 +1,10 @@ import { Box, List, ListItem, ListItemIcon, ListItemText } from "@mui/material" -import { difference, groupBy } from "lodash" +import { groupBy } from "lodash" import { Fragment, memo, useMemo } from "react" import { Report } from "../debug/report.shared" import { TopBarNavigation } from "../navigation/TopBarNavigation" import { BuildRounded } from "@mui/icons-material" import { useFixCollections } from "./useFixCollections" -import { useFixBookReferences } from "./useFixBookReferences" -import { useDuplicatedResourceIdLinks } from "./useDuplicateLinks" -import { useFixBooksDanglingLinks } from "./useFixBooksDanglingLinks" -import { useBooksDanglingLinks } from "./useBooksDanglingLinks" import { useDuplicatedBookTitles, useFixDuplicatedBookTitles @@ -18,37 +14,27 @@ import { useObserve } from "reactjrx" import { latestDatabase$ } from "../rxdb/useCreateDatabase" import { switchMap } from "rxjs" import { getMetadataFromCollection } from "../collections/getMetadataFromCollection" +import { useFixableCollections } from "./useFixableCollections" +import { useRepair } from "./useRepair" +import { CollectionDanglingBooks } from "./CollectionDanglingBooks" +import { useFixableBooks } from "./useFixableBooks" +import { BookDanglingCollections } from "./BookDanglingCollections" +import { BookDanglingLinks } from "./BookDanglingLinks" export const ProblemsScreen = memo(() => { const fixCollections = useFixCollections() - const fixBookReferences = useFixBookReferences() - const fixBooksDanglingLinks = useFixBooksDanglingLinks() - const duplicatedLinks = useDuplicatedResourceIdLinks() const duplicatedBookTitles = useDuplicatedBookTitles() const fixDuplicatedBookTitles = useFixDuplicatedBookTitles() + const { collectionsWithDanglingBooks } = useFixableCollections() + const { booksWithDanglingCollections, booksWithDanglingLinks } = useFixableBooks() + const { mutate: repair } = useRepair() const collections = useObserve( () => latestDatabase$.pipe(switchMap((db) => db.obokucollection.find().$)), [] ) - const books = useObserve( - () => latestDatabase$.pipe(switchMap((db) => db.book.find().$)), - [] - ) - const collectionIds = useMemo( - () => collections?.map((doc) => doc._id), - [collections] - ) - const bookIds = useMemo(() => books?.map((doc) => doc._id), [books]) - const booksWithInvalidCollections = books?.filter( - (doc) => difference(doc.collections, collectionIds ?? []).length > 0 - ) - const collectionsWithNonExistingBooks = collections?.filter( - (doc) => difference(doc.books, bookIds ?? []).length > 0 - ) - const booksWithDanglingLinks = useBooksDanglingLinks() const duplicatedCollections = useMemo(() => { - const collectionsByResourceId = groupBy(collections, "resourceId") + const collectionsByResourceId = groupBy(collections, "linkResourceId") const duplicatedCollections = Object.keys(collectionsByResourceId) .filter((resourceId) => collectionsByResourceId[resourceId]!.length > 1) .map((resourceId) => [ @@ -69,13 +55,6 @@ export const ProblemsScreen = memo(() => { return duplicatedCollections as [string, { name: string; number: number }][] }, [collections]) - Report.log({ - books, - duplicatedBookTitles, - booksWithDanglingLinks, - duplicatedLinks - }) - return ( <> @@ -144,39 +123,34 @@ export const ProblemsScreen = memo(() => { /> )} - {!!collections && !!booksWithInvalidCollections?.length && ( - fixBookReferences(booksWithInvalidCollections)} - > - - - - - - )} - {!!books && !!collectionsWithNonExistingBooks?.length && ( - fixBookReferences(collectionsWithNonExistingBooks)} - > - - - - - - )} + {booksWithDanglingCollections?.map(({ danglingItems, doc }) => ( + + repair({ + danglingItems, + doc, + type: "bookDanglingCollections" + }) + } + /> + ))} + {collectionsWithDanglingBooks?.map(({ doc, danglingItems }) => ( + + repair({ + danglingItems, + doc, + type: "collectionDanglingBooks" + }) + } + /> + ))} {/* {duplicatedLinks.length > 0 && ( { /> )} */} - {!!booksWithDanglingLinks?.length && ( - fixBooksDanglingLinks(booksWithDanglingLinks)} - > - - - - - - )} + {booksWithDanglingLinks?.map(({ doc, danglingItems }) => ( + + repair({ + danglingItems, + doc, + type: "bookDanglingLinks" + }) + } + /> + ))} {duplicatedBookTitles.length > 0 && ( { - const books = useObserve( - () => latestDatabase$.pipe(switchMap((db) => db?.book.find().$)), - [] - ) - const links = useObserve( - () => latestDatabase$.pipe(switchMap((db) => db?.link.find().$)), - [] - ) - - return useMemo(() => { - return books?.filter( - (doc) => - difference(doc.links, links?.map((doc) => doc._id) ?? []).length > 0 - ) - }, [books, links]) -} diff --git a/packages/web/src/problems/useFixBookReferences.ts b/packages/web/src/problems/useFixBookReferences.ts deleted file mode 100644 index 155eb202..00000000 --- a/packages/web/src/problems/useFixBookReferences.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { difference } from "lodash" -import { useCallback } from "react" -import { Report } from "../debug/report.shared" -import { useDatabase } from "../rxdb" -import { BookDocument } from "../rxdb/collections/book" - -export const useFixBookReferences = () => { - const { db } = useDatabase() - - const removeDanglingCollectionsFromBook = useCallback( - async (doc: BookDocument) => { - if (doc.collections.length === 0) return - - const existingCollection = await db?.obokucollection - .safeFind({ - selector: { - _id: { - $in: doc.collections - } - } - }) - .exec() - - const toRemove = difference( - doc.collections, - existingCollection?.map((doc) => doc._id) ?? [] - ) - - if (toRemove.length > 0) { - await doc.incrementalModify((data) => ({ - ...data, - collections: data.collections.filter((id) => !toRemove.includes(id)) - })) - } - }, - [db] - ) - - return useCallback( - async (data: BookDocument[]) => { - const yes = window.confirm( - ` - This action will remove non valid collection reference from all the books. - `.replace(/ +/g, "") - ) - - if (yes && db) { - try { - // we actually have middleware to deal with it so we will just force an update - Promise.all(data.map(removeDanglingCollectionsFromBook)) - } catch (e) { - Report.error(e) - } - } - }, - [db, removeDanglingCollectionsFromBook] - ) -} diff --git a/packages/web/src/problems/useFixBooksDanglingLinks.ts b/packages/web/src/problems/useFixBooksDanglingLinks.ts deleted file mode 100644 index 4cd1830f..00000000 --- a/packages/web/src/problems/useFixBooksDanglingLinks.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { difference } from "lodash" -import { useCallback } from "react" -import { Report } from "../debug/report.shared" -import { useDatabase } from "../rxdb" -import { BookDocument } from "../rxdb/collections/book" - -export const useFixBooksDanglingLinks = () => { - const { db } = useDatabase() - - const removeDanglingLinksFromBook = useCallback( - async (doc: BookDocument) => { - if (doc.links.length === 0) return - - const existingLinksForThisBook = await db?.link - .safeFind({ - selector: { - _id: { - $in: doc.links - } - } - }) - .exec() - - const toRemove = difference( - doc.links, - existingLinksForThisBook?.map((doc) => doc._id) ?? [] - ) - - if (toRemove.length > 0) { - await doc.incrementalModify((data) => ({ - ...data, - links: data.links.filter((id) => !toRemove.includes(id)) - })) - } - }, - [db] - ) - - return useCallback( - async (data: BookDocument[]) => { - const yes = window.confirm( - ` - This action will remove invalid links from books. - `.replace(/ +/g, "") - ) - - if (yes && db) { - try { - // we actually have middleware to deal with it so we will just force an update - Promise.all(data.map(removeDanglingLinksFromBook)) - } catch (e) { - Report.error(e) - } - } - }, - [db, removeDanglingLinksFromBook] - ) -} diff --git a/packages/web/src/problems/useFixableBooks.ts b/packages/web/src/problems/useFixableBooks.ts new file mode 100644 index 00000000..f7f75630 --- /dev/null +++ b/packages/web/src/problems/useFixableBooks.ts @@ -0,0 +1,67 @@ +import { useMemo } from "react" +import { useBooks } from "../books/states" +import { useCollections } from "../collections/useCollections" +import { difference } from "lodash" +import { BookDocType } from "@oboku/shared" +import { DeepReadonlyObject } from "rxdb" +import { useLinks } from "../links/states" + +export const useFixableBooks = () => { + const { data: unsafeCollections } = useCollections({ + includeProtected: true + }) + const { data: unsafeBooks } = useBooks({ includeProtected: true }) + const { data: links } = useLinks() + + const unsafeCollectionIds = useMemo( + () => unsafeCollections?.map((item) => item._id), + [unsafeCollections] + ) + const linkIds = useMemo(() => links?.map((item) => item._id), [links]) + + const booksWithDanglingCollections = unsafeBooks?.reduce( + (acc, doc) => { + const danglingItems = difference( + doc.collections, + unsafeCollectionIds ?? [] + ) + + if (danglingItems.length > 0) { + return [ + ...acc, + { + doc, + danglingItems + } + ] + } + + return acc + }, + [] as { doc: DeepReadonlyObject; danglingItems: string[] }[] + ) + + const booksWithDanglingLinks = unsafeBooks?.reduce( + (acc, doc) => { + const danglingItems = difference( + doc.links, + linkIds ?? [] + ) + + if (danglingItems.length > 0) { + return [ + ...acc, + { + doc, + danglingItems + } + ] + } + + return acc + }, + [] as { doc: DeepReadonlyObject; danglingItems: string[] }[] + ) + + return { booksWithDanglingCollections, booksWithDanglingLinks } +} diff --git a/packages/web/src/problems/useFixableCollections.ts b/packages/web/src/problems/useFixableCollections.ts new file mode 100644 index 00000000..c0d2425c --- /dev/null +++ b/packages/web/src/problems/useFixableCollections.ts @@ -0,0 +1,37 @@ +import { useMemo } from "react" +import { useBooks } from "../books/states" +import { useCollections } from "../collections/useCollections" +import { difference } from "lodash" +import { CollectionDocType } from "@oboku/shared" + +export const useFixableCollections = () => { + const { data: unsafeCollections } = useCollections({ + includeProtected: true + }) + const { data: unsafeBooks } = useBooks({ includeProtected: true }) + const unsafeBookIds = useMemo( + () => unsafeBooks?.map((item) => item._id), + [unsafeBooks] + ) + + const collectionsWithDanglingBooks = unsafeCollections?.reduce( + (acc, doc) => { + const danglingItems = difference(doc.books, unsafeBookIds ?? []) + + if (danglingItems.length > 0) { + return [ + ...acc, + { + doc, + danglingItems + } + ] + } + + return acc + }, + [] as { doc: CollectionDocType; danglingItems: string[] }[] + ) + + return { collectionsWithDanglingBooks } +} diff --git a/packages/web/src/problems/useRepair.ts b/packages/web/src/problems/useRepair.ts new file mode 100644 index 00000000..2062a854 --- /dev/null +++ b/packages/web/src/problems/useRepair.ts @@ -0,0 +1,143 @@ +import { BookDocType, CollectionDocType } from "@oboku/shared" +import { useMutation } from "reactjrx" +import { first, from, map, mergeMap, of } from "rxjs" +import { latestDatabase$ } from "../rxdb/useCreateDatabase" +import { DeepReadonlyObject } from "rxdb" + +export const useRepair = () => { + return useMutation({ + mutationFn: ( + action: + | { + type: "collectionDanglingBooks" + doc: CollectionDocType + danglingItems: string[] + } + | { + type: "bookDanglingCollections" + doc: DeepReadonlyObject + danglingItems: string[] + } + | { + type: "bookDanglingLinks" + doc: DeepReadonlyObject + danglingItems: string[] + } + ) => { + const db$ = latestDatabase$.pipe(first()) + + if (action.type === "collectionDanglingBooks") { + const yes = window.confirm( + ` + This action will remove the invalid book references from the collection. It will not remove anything else. + `.replace(/ +/g, "") + ) + + if (!yes) return of(null) + + return db$.pipe( + mergeMap((db) => + from( + db.obokucollection + .findOne({ selector: { _id: action.doc._id } }) + .exec() + ).pipe( + mergeMap((item) => { + if (!item) return of(null) + + return from( + item.incrementalModify((old) => { + const nonDanglingBooks = old.books.filter( + (id) => !action.danglingItems.includes(id) + ) + + return { + ...old, + books: nonDanglingBooks + } + }) + ) + }) + ) + ), + map(() => null) + ) + } + + if (action.type === "bookDanglingCollections") { + const yes = window.confirm( + ` + This action will remove the invalid collection references from the book. It will not remove anything else. + `.replace(/ +/g, "") + ) + + if (!yes) return of(null) + + return db$.pipe( + mergeMap((db) => + from( + db.book.findOne({ selector: { _id: action.doc._id } }).exec() + ).pipe( + mergeMap((item) => { + if (!item) return of(null) + + return from( + item.incrementalModify((old) => { + const nonDanglingCollections = old.collections.filter( + (id) => !action.danglingItems.includes(id) + ) + + return { + ...old, + collections: nonDanglingCollections + } + }) + ) + }) + ) + ), + map(() => null) + ) + } + + if (action.type === "bookDanglingLinks") { + const yes = window.confirm( + ` + This action will remove the invalid link references from the book. It will not remove anything else. + `.replace(/ +/g, "") + ) + + if (!yes) return of(null) + + return db$.pipe( + mergeMap((db) => + from( + db.book.findOne({ selector: { _id: action.doc._id } }).exec() + ).pipe( + mergeMap((item) => { + if (!item) return of(null) + + return from( + item.incrementalModify((old) => { + const nonDanglingLinks = old.links.filter( + (id) => !action.danglingItems.includes(id) + ) + + return { + ...old, + _meta: old._meta ?? {}, + links: nonDanglingLinks + } + }) + ) + }) + ) + ), + map(() => null) + ) + } + + return of(null) + } + }) +} diff --git a/packages/web/src/profile/index.ts b/packages/web/src/profile/index.ts index ee799674..0c3d5249 100644 --- a/packages/web/src/profile/index.ts +++ b/packages/web/src/profile/index.ts @@ -4,7 +4,7 @@ import { libraryStateSignal } from "../library/states" import { readerSettingsStateSignal } from "../reader/settings/states" import { bookBeingReadStatePersist } from "../reading/states" import { localSettingsStatePersist } from "../settings/states" -import { collectionsListSignal } from "../library/collections/state" +import { collectionsListSignal } from "../library/shelves/state" import { collectionDetailsScreenListControlsStateSignal } from "../collections/CollectionDetailsScreen" import { searchListActionsToolbarSignal } from "../search/list/states" import { SignalPersistenceConfig } from "reactjrx" diff --git a/packages/web/src/reader/navigation/BottomBar.tsx b/packages/web/src/reader/navigation/BottomBar.tsx index d530804e..38a8ae38 100644 --- a/packages/web/src/reader/navigation/BottomBar.tsx +++ b/packages/web/src/reader/navigation/BottomBar.tsx @@ -21,7 +21,6 @@ export const BottomBar = () => { const reader = useSignalValue(readerStateSignal) const navigation = useObserve(reader?.navigation.state$ ?? NEVER) const { data: pagination } = usePagination() - // const showScrubber = (totalPages || 1) > 1 const showScrubber = true const { useOptimizedTheme } = useLocalSettings() diff --git a/packages/web/src/rxdb/collections/book.ts b/packages/web/src/rxdb/collections/book.ts index 6b916d05..2d70541c 100644 --- a/packages/web/src/rxdb/collections/book.ts +++ b/packages/web/src/rxdb/collections/book.ts @@ -5,6 +5,7 @@ import { } from "@oboku/shared" import { AtomicUpdateFunction, + MigrationStrategies, RxCollection, RxDocument, RxJsonSchema, @@ -71,7 +72,7 @@ export const bookCollectionMethods: BookCollectionMethods = { } } -export const bookSchemaMigrationStrategies = {} +export const bookSchemaMigrationStrategies: MigrationStrategies = {} export const bookSchema: RxJsonSchema< Omit diff --git a/packages/web/src/search/SearchScreen.tsx b/packages/web/src/search/SearchScreen.tsx index 0e021828..e7c97de2 100644 --- a/packages/web/src/search/SearchScreen.tsx +++ b/packages/web/src/search/SearchScreen.tsx @@ -69,8 +69,20 @@ const SeeMore = ({ ) } +const useClasses = makeStyles((theme) => ({ + inputRoot: { + color: "inherit", + width: "100%" + }, + inputInput: { + padding: theme.spacing(1, 1, 1, 1), + width: "100%" + } +})) + export const SearchScreen = () => { - const { styles, classes } = useStyles() + const { styles } = useStyles() + const classes = useClasses() const [searchParams, setSearchParams] = useSearchParams() const value = useSignalValue(searchStateSignal) const { data: collections = [] } = useCollectionsForSearch(value) @@ -210,20 +222,8 @@ export const SearchScreen = () => { ) } -const useClasses = makeStyles((theme) => ({ - inputRoot: { - color: "inherit", - width: "100%" - }, - inputInput: { - padding: theme.spacing(1, 1, 1, 1), - width: "100%" - } -})) - const useStyles = () => { const theme = useTheme() - const classes = useClasses() const styles = useCSS( () => ({ @@ -248,5 +248,5 @@ const useStyles = () => { [theme] ) - return { styles, classes } + return { styles } } diff --git a/packages/web/src/search/useBooksForSearch.ts b/packages/web/src/search/useBooksForSearch.ts index 207ec068..11a684bc 100644 --- a/packages/web/src/search/useBooksForSearch.ts +++ b/packages/web/src/search/useBooksForSearch.ts @@ -1,17 +1,17 @@ import { getMetadataFromBook } from "../books/metadata" import { REGEXP_SPECIAL_CHAR } from "./useCollectionsForSearch" import { sortByTitleComparator } from "@oboku/shared" -import { useVisibleBooks } from "../books/useVisibleBooks" import { useMemo } from "react" import { useSignalValue } from "reactjrx" import { searchListActionsToolbarSignal } from "./list/states" +import { useBooks } from "../books/states" export const useBooksForSearch = (search: string) => { const { notInterestedContents } = useSignalValue( searchListActionsToolbarSignal ) - const { data: visibleBooks } = useVisibleBooks({ + const { data: visibleBooks } = useBooks({ isNotInterested: notInterestedContents }) @@ -19,13 +19,19 @@ export const useBooksForSearch = (search: string) => { () => visibleBooks ?.filter((book) => { - return book.metadata?.some(({ title }) => { - if (!title) return false + const searchRegex = new RegExp( + search.replace(REGEXP_SPECIAL_CHAR, `\\$&`) || "", + "i" + ) + + const metadata = book.metadata?.length + ? book.metadata + : [getMetadataFromBook(book)] - const searchRegex = new RegExp( - search.replace(REGEXP_SPECIAL_CHAR, `\\$&`) || "", - "i" - ) + return metadata?.some((item) => { + const { title } = item + + if (!title) return false const indexOfFirstMatch = title?.search(searchRegex) || 0 diff --git a/packages/web/src/settings/ManageStorageScreen.tsx b/packages/web/src/settings/ManageStorageScreen.tsx index 34aa0ebf..67b632cb 100644 --- a/packages/web/src/settings/ManageStorageScreen.tsx +++ b/packages/web/src/settings/ManageStorageScreen.tsx @@ -20,10 +20,6 @@ import { } from "@mui/icons-material" import { useStorageUse } from "./useStorageUse" import { BookList } from "../books/bookList/BookList" -import { - useDownloadedBookWithUnsafeProtectedIdsState, - useVisibleBookIds -} from "../books/states" import { bookActionDrawerSignal } from "../books/drawer/BookActionsDrawer" import { useDownloadedFilesInfo } from "../download/useDownloadedFilesInfo" import { useRemoveDownloadFile } from "../download/useRemoveDownloadFile" @@ -34,11 +30,17 @@ import { useEffect } from "react" import { useMutation } from "reactjrx" import { useRemoveAllDownloadedFiles } from "../download/useRemoveAllDownloadedFiles" import { useRemoveCoversInCache } from "../covers/useRemoveCoversInCache" +import { useDownloadedBooks } from "../download/useDownloadedBooks" +import { useBooks } from "../books/states" export const ManageStorageScreen = () => { - const books = useDownloadedBookWithUnsafeProtectedIdsState() + const books = useDownloadedBooks() const bookIds = useMemo(() => books?.map((book) => book._id) ?? [], [books]) - const visibleBookIds = useVisibleBookIds() + const { data: visibleBooks } = useBooks() + const visibleBookIds = useMemo( + () => visibleBooks?.map((item) => item._id) ?? [], + [visibleBooks] + ) const { quotaUsed, quotaInGb, usedInMb, covers, coversWightInMb } = useStorageUse([books]) const { mutate: removeCoversInCache } = useRemoveCoversInCache() diff --git a/packages/web/src/settings/StatisticsScreen.tsx b/packages/web/src/settings/StatisticsScreen.tsx index 8261b083..d99b63af 100644 --- a/packages/web/src/settings/StatisticsScreen.tsx +++ b/packages/web/src/settings/StatisticsScreen.tsx @@ -1,7 +1,7 @@ import { TopBarNavigation } from "../navigation/TopBarNavigation" import { Box, List, ListItem, ListItemText, ListSubheader } from "@mui/material" -import { useBooks } from "../books/states" import { useCollections } from "../collections/useCollections" +import { useBooks } from "../books/states" export const StatisticsScreen = () => { const { data: books } = useBooks()