diff options
42 files changed, 768 insertions, 246 deletions
diff --git a/docs/usage/shortcuts.rst b/docs/usage/shortcuts.rst index 6465f28..8832977 100644 --- a/docs/usage/shortcuts.rst +++ b/docs/usage/shortcuts.rst @@ -15,7 +15,7 @@ Navigation :header-rows: 1 * - Shortcut - - Navigates to + - Navigates to... * - ``go`` - Home * - ``gc`` @@ -24,14 +24,14 @@ Navigation - Namespaces * - ``gt`` - Tags - * - ``gh`` - - Characters - * - ``gw`` - - Worlds * - ``ga`` - Artists * - ``gi`` - Circles + * - ``gh`` + - Characters + * - ``gw`` + - Worlds * - ``gz`` - Archives * - ``gs`` @@ -74,6 +74,8 @@ Reader * - Shortcut - Action + * - ``f`` + - Toggle fullscreen. * - ``z`` - Open edit menu. * - ``Escape`` @@ -90,15 +92,67 @@ Filtering * - Shortcut - Action - * - ``F`` + * - ``q`` - Focus search. * - ``f`` - Toggle favourites. * - ``b`` - Toggle bookmarked. - * - ``o`` + * - ``r`` + - Toggle orphaned. + * - ``z`` - Toggle organized. +.. _shortcut-advanced-filtering: + +Advanced Filtering +------------------ + +.. list-table:: + :align: left + :header-rows: 1 + + * - Shortcut + - Action + * - ``F`` + - Toggle advanced filters. + * - ``X`` + - Reset filters. + +Focusing filter fields +^^^^^^^^^^^^^^^^^^^^^^ + +When the advanced filters are visible, each field may be focused by activating +the following shortcuts prefixed by either ``i`` for an include field or ``e`` +for an exclude field. For example, to include an artist, hit ``ia``. + +.. list-table:: + :align: left + :header-rows: 1 + + * - Shortcut + - Filters on... + * - ``t`` + - Tags + * - ``a`` + - Artists + * - ``i`` + - Circles + * - ``h`` + - Characters + * - ``w`` + - Worlds + * - ``g`` + - Categories + * - ``r`` + - Ratings + * - ``s`` + - Censorships + * - ``l`` + - Languages + * - ``u`` + - URLs + .. _shortcut-misc: Miscellaneous diff --git a/frontend/operations.graphql b/frontend/operations.graphql index 60efc09..09364f6 100644 --- a/frontend/operations.graphql +++ b/frontend/operations.graphql @@ -22,6 +22,8 @@ fragment Comic on Comic { title originalTitle favourite + date + pageCount cover { ...Image } diff --git a/frontend/src/app.css b/frontend/src/app.css index 6a8f782..bb6ae9b 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -215,6 +215,10 @@ --sv-dropdown-active-bg: var(--color-rose-800); } +.exclude .svelecte.is-focused { + --sv-border: 1px solid var(--color-red-500); +} + .sv-item--btn { border-radius: 0 2px 2px 0 !important; } diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index bd001f3..03fd972 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -122,6 +122,7 @@ export type Artist = { }; export type ArtistFilter = { + comics?: InputMaybe<BasicCountFilter>; name?: InputMaybe<StringFilter>; }; @@ -139,6 +140,7 @@ export type ArtistFilterResult = { export type ArtistResponse = Artist | IdNotFoundError; export enum ArtistSort { + ComicCount = 'COMIC_COUNT', CreatedAt = 'CREATED_AT', Name = 'NAME', Random = 'RANDOM', @@ -164,10 +166,14 @@ export type ArtistsUpsertInput = { export type AssociationFilter = { all?: InputMaybe<Array<Scalars['Int']['input']>>; any?: InputMaybe<Array<Scalars['Int']['input']>>; - empty?: InputMaybe<Scalars['Boolean']['input']>; + count?: InputMaybe<CountFilter>; exact?: InputMaybe<Array<Scalars['Int']['input']>>; }; +export type BasicCountFilter = { + count: CountFilter; +}; + export enum Category { Artbook = 'ARTBOOK', Comic = 'COMIC', @@ -203,6 +209,7 @@ export type Character = { }; export type CharacterFilter = { + comics?: InputMaybe<BasicCountFilter>; name?: InputMaybe<StringFilter>; }; @@ -220,6 +227,7 @@ export type CharacterFilterResult = { export type CharacterResponse = Character | IdNotFoundError; export enum CharacterSort { + ComicCount = 'COMIC_COUNT', CreatedAt = 'CREATED_AT', Name = 'NAME', Random = 'RANDOM', @@ -249,6 +257,7 @@ export type Circle = { }; export type CircleFilter = { + comics?: InputMaybe<BasicCountFilter>; name?: InputMaybe<StringFilter>; }; @@ -266,6 +275,7 @@ export type CircleFilterResult = { export type CircleResponse = Circle | IdNotFoundError; export enum CircleSort { + ComicCount = 'COMIC_COUNT', CreatedAt = 'CREATED_AT', Name = 'NAME', Random = 'RANDOM', @@ -348,6 +358,9 @@ export type ComicScraper = { }; export enum ComicSort { + ArtistCount = 'ARTIST_COUNT', + CharacterCount = 'CHARACTER_COUNT', + CircleCount = 'CIRCLE_COUNT', CreatedAt = 'CREATED_AT', Date = 'DATE', OriginalTitle = 'ORIGINAL_TITLE', @@ -355,7 +368,8 @@ export enum ComicSort { Random = 'RANDOM', TagCount = 'TAG_COUNT', Title = 'TITLE', - UpdatedAt = 'UPDATED_AT' + UpdatedAt = 'UPDATED_AT', + WorldCount = 'WORLD_COUNT' } export type ComicSortInput = { @@ -396,6 +410,11 @@ export type ComicTotals = { worlds: Scalars['Int']['output']; }; +export type CountFilter = { + operator?: InputMaybe<Operator>; + value: Scalars['Int']['input']; +}; + export type CoverInput = { id: Scalars['Int']['input']; }; @@ -862,6 +881,7 @@ export type Namespace = { export type NamespaceFilter = { name?: InputMaybe<StringFilter>; + tags?: InputMaybe<BasicCountFilter>; }; export type NamespaceFilterInput = { @@ -882,6 +902,7 @@ export enum NamespaceSort { Name = 'NAME', Random = 'RANDOM', SortName = 'SORT_NAME', + TagCount = 'TAG_COUNT', UpdatedAt = 'UPDATED_AT' } @@ -905,6 +926,12 @@ export enum OnMissing { Ignore = 'IGNORE' } +export enum Operator { + Equal = 'EQUAL', + GreaterThan = 'GREATER_THAN', + LowerThan = 'LOWER_THAN' +} + export type Page = { __typename?: 'Page'; comicId?: Maybe<Scalars['Int']['output']>; @@ -1153,11 +1180,12 @@ export type Tag = { export type TagAssociationFilter = { all?: InputMaybe<Array<Scalars['String']['input']>>; any?: InputMaybe<Array<Scalars['String']['input']>>; - empty?: InputMaybe<Scalars['Boolean']['input']>; + count?: InputMaybe<CountFilter>; exact?: InputMaybe<Array<Scalars['String']['input']>>; }; export type TagFilter = { + comics?: InputMaybe<BasicCountFilter>; name?: InputMaybe<StringFilter>; namespaces?: InputMaybe<AssociationFilter>; }; @@ -1176,8 +1204,10 @@ export type TagFilterResult = { export type TagResponse = FullTag | IdNotFoundError; export enum TagSort { + ComicCount = 'COMIC_COUNT', CreatedAt = 'CREATED_AT', Name = 'NAME', + NamespaceCount = 'NAMESPACE_COUNT', Random = 'RANDOM', UpdatedAt = 'UPDATED_AT' } @@ -1324,6 +1354,7 @@ export type World = { }; export type WorldFilter = { + comics?: InputMaybe<BasicCountFilter>; name?: InputMaybe<StringFilter>; }; @@ -1341,6 +1372,7 @@ export type WorldFilterResult = { export type WorldResponse = IdNotFoundError | World; export enum WorldSort { + ComicCount = 'COMIC_COUNT', CreatedAt = 'CREATED_AT', Name = 'NAME', Random = 'RANDOM', @@ -1367,9 +1399,9 @@ export type ImageFragment = { __typename?: 'Image', hash: string, width: number, export type PageFragment = { __typename?: 'Page', id: number, path: string, comicId?: number | null, image: { __typename?: 'Image', id: number, hash: string, aspectRatio: number, width: number, height: number } }; -export type ComicFragment = { __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }; +export type ComicFragment = { __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }; -export type FullArchiveFragment = { __typename?: 'FullArchive', id: number, name: string, path: string, size: number, createdAt: string, mtime: string, organized: boolean, pageCount: number, pages: Array<{ __typename?: 'Page', id: number, path: string, comicId?: number | null, image: { __typename?: 'Image', id: number, hash: string, aspectRatio: number, width: number, height: number } }>, comics: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }; +export type FullArchiveFragment = { __typename?: 'FullArchive', id: number, name: string, path: string, size: number, createdAt: string, mtime: string, organized: boolean, pageCount: number, pages: Array<{ __typename?: 'Page', id: number, path: string, comicId?: number | null, image: { __typename?: 'Image', id: number, hash: string, aspectRatio: number, width: number, height: number } }>, comics: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }; export type ArchiveFragment = { __typename?: 'Archive', id: number, name: string, size: number, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number } }; @@ -1386,7 +1418,7 @@ export type ComicsQueryVariables = Exact<{ }>; -export type ComicsQuery = { __typename?: 'Query', comics: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } }; +export type ComicsQuery = { __typename?: 'Query', comics: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } }; export type ArchivesQueryVariables = Exact<{ pagination: Pagination; @@ -1402,7 +1434,7 @@ export type ArchiveQueryVariables = Exact<{ }>; -export type ArchiveQuery = { __typename?: 'Query', archive: { __typename?: 'FullArchive', id: number, name: string, path: string, size: number, createdAt: string, mtime: string, organized: boolean, pageCount: number, pages: Array<{ __typename?: 'Page', id: number, path: string, comicId?: number | null, image: { __typename?: 'Image', id: number, hash: string, aspectRatio: number, width: number, height: number } }>, comics: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } | { __typename?: 'IDNotFoundError', message: string } }; +export type ArchiveQuery = { __typename?: 'Query', archive: { __typename?: 'FullArchive', id: number, name: string, path: string, size: number, createdAt: string, mtime: string, organized: boolean, pageCount: number, pages: Array<{ __typename?: 'Page', id: number, path: string, comicId?: number | null, image: { __typename?: 'Image', id: number, hash: string, aspectRatio: number, width: number, height: number } }>, comics: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } | { __typename?: 'IDNotFoundError', message: string } }; export type ComicQueryVariables = Exact<{ id: Scalars['Int']['input']; @@ -1559,7 +1591,7 @@ export type FrontpageQueryVariables = Exact<{ }>; -export type FrontpageQuery = { __typename?: 'Query', recent: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }, favourites: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }, bookmarked: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } }; +export type FrontpageQuery = { __typename?: 'Query', recent: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }, favourites: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> }, bookmarked: { __typename?: 'ComicFilterResult', count: number, edges: Array<{ __typename?: 'Comic', id: number, title: string, originalTitle?: string | null, favourite: boolean, date?: string | null, pageCount: number, cover: { __typename?: 'Image', hash: string, width: number, height: number }, tags: Array<{ __typename?: 'ComicTag', name: string, description?: string | null }>, artists: Array<{ __typename?: 'Artist', name: string }>, characters: Array<{ __typename?: 'Character', name: string }>, worlds: Array<{ __typename?: 'World', name: string }>, circles: Array<{ __typename?: 'Circle', name: string }> }> } }; export type StatisticsQueryVariables = Exact<{ [key: string]: never; }>; @@ -1745,15 +1777,15 @@ export type DeleteWorldsMutation = { __typename?: 'Mutation', deleteWorlds: { __ export const PageFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}}]} as unknown as DocumentNode<PageFragment, unknown>; export const ImageFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}}]} as unknown as DocumentNode<ImageFragment, unknown>; -export const ComicFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}}]} as unknown as DocumentNode<ComicFragment, unknown>; -export const FullArchiveFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullArchive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mtime"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<FullArchiveFragment, unknown>; +export const ComicFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}}]} as unknown as DocumentNode<ComicFragment, unknown>; +export const FullArchiveFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullArchive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mtime"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<FullArchiveFragment, unknown>; export const ArchiveFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Archive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Archive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}}]} as unknown as DocumentNode<ArchiveFragment, unknown>; export const FullComicFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullComic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullComic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"censorship"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"bookmarked"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}}]} as unknown as DocumentNode<FullComicFragment, unknown>; export const ComicScraperFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ComicScraper"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ComicScraper"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<ComicScraperFragment, unknown>; export const ScrapeComicResultFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScrapeComicResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScrapeComicResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"artists"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"censorship"}},{"kind":"Field","name":{"kind":"Name","value":"characters"}},{"kind":"Field","name":{"kind":"Name","value":"circles"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"worlds"}}]}},{"kind":"Field","name":{"kind":"Name","value":"warnings"}}]}}]} as unknown as DocumentNode<ScrapeComicResultFragment, unknown>; -export const ComicsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"comics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Pagination"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ComicFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ComicSortInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ComicsQuery, ComicsQueryVariables>; +export const ComicsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"comics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Pagination"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ComicFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ComicSortInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ComicsQuery, ComicsQueryVariables>; export const ArchivesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"archives"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Pagination"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ArchiveFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ArchiveSortInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archives"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Archive"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Archive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Archive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}}]} as unknown as DocumentNode<ArchivesQuery, ArchivesQueryVariables>; -export const ArchiveDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"archive"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullArchive"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullArchive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mtime"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}}]}}]} as unknown as DocumentNode<ArchiveQuery, ArchiveQueryVariables>; +export const ArchiveDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"archive"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullArchive"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullArchive"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullArchive"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mtime"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}}]}}]} as unknown as DocumentNode<ArchiveQuery, ArchiveQueryVariables>; export const ComicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"comic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullComic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullComic"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Page"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Page"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"aspectRatio"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comicId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullComic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullComic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"censorship"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"organized"}},{"kind":"Field","name":{"kind":"Name","value":"bookmarked"}},{"kind":"Field","name":{"kind":"Name","value":"pages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Page"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ComicQuery, ComicQueryVariables>; export const TagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"tag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullTag"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"namespaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode<TagQuery, TagQueryVariables>; export const TagsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"tags"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Pagination"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"TagFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"TagSortInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tags"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]} as unknown as DocumentNode<TagsQuery, TagsQueryVariables>; @@ -1775,7 +1807,7 @@ export const WorldsDocument = {"kind":"Document","definitions":[{"kind":"Operati export const WorldDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"world"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"world"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"World"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode<WorldQuery, WorldQueryVariables>; export const ComicScrapersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"comicScrapers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comicScrapers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ComicScrapersQuery, ComicScrapersQueryVariables>; export const ScrapeComicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"scrapeComic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"scraper"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scrapeComic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"scraper"},"value":{"kind":"Variable","name":{"kind":"Name","value":"scraper"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScrapeComicResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScrapeComicResult"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScrapeComicResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScrapeComicResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"artists"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"censorship"}},{"kind":"Field","name":{"kind":"Name","value":"characters"}},{"kind":"Field","name":{"kind":"Name","value":"circles"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"worlds"}}]}},{"kind":"Field","name":{"kind":"Name","value":"warnings"}}]}}]} as unknown as DocumentNode<ScrapeComicQuery, ScrapeComicQueryVariables>; -export const FrontpageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"frontpage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"seed"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"recent"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"CREATED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESCENDING"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"favourites"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"include"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"favourite"},"value":{"kind":"BooleanValue","value":true}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"RANDOM"}},{"kind":"ObjectField","name":{"kind":"Name","value":"seed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"seed"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"bookmarked"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"include"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"bookmarked"},"value":{"kind":"BooleanValue","value":true}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"RANDOM"}},{"kind":"ObjectField","name":{"kind":"Name","value":"seed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"seed"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<FrontpageQuery, FrontpageQueryVariables>; +export const FrontpageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"frontpage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"seed"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"recent"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"CREATED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESCENDING"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"favourites"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"include"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"favourite"},"value":{"kind":"BooleanValue","value":true}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"RANDOM"}},{"kind":"ObjectField","name":{"kind":"Name","value":"seed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"seed"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"bookmarked"},"name":{"kind":"Name","value":"comics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"items"},"value":{"kind":"IntValue","value":"6"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"include"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"bookmarked"},"value":{"kind":"BooleanValue","value":true}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"on"},"value":{"kind":"EnumValue","value":"RANDOM"}},{"kind":"ObjectField","name":{"kind":"Name","value":"seed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"seed"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Comic"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Image"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Comic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"originalTitle"}},{"kind":"Field","name":{"kind":"Name","value":"favourite"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"pageCount"}},{"kind":"Field","name":{"kind":"Name","value":"cover"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"artists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"characters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"worlds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<FrontpageQuery, FrontpageQueryVariables>; export const StatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"statistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archives"}},{"kind":"Field","name":{"kind":"Name","value":"artists"}},{"kind":"Field","name":{"kind":"Name","value":"characters"}},{"kind":"Field","name":{"kind":"Name","value":"circles"}},{"kind":"Field","name":{"kind":"Name","value":"comic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"artists"}},{"kind":"Field","name":{"kind":"Name","value":"characters"}},{"kind":"Field","name":{"kind":"Name","value":"circles"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"worlds"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comics"}},{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"scrapers"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}},{"kind":"Field","name":{"kind":"Name","value":"namespaces"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"worlds"}}]}}]}}]}}]} as unknown as DocumentNode<StatisticsQuery, StatisticsQueryVariables>; export const AddComicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"addComic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AddComicInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addComic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AddComicSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"archivePagesRemaining"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode<AddComicMutation, AddComicMutationVariables>; export const UpdateArchivesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateArchives"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateArchiveInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateArchives"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Success"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateArchivesMutation, UpdateArchivesMutationVariables>; diff --git a/frontend/src/lib/Enums.ts b/frontend/src/lib/Enums.ts index 3264de4..292ed7d 100644 --- a/frontend/src/lib/Enums.ts +++ b/frontend/src/lib/Enums.ts @@ -10,6 +10,7 @@ import { Language, Layout, NamespaceSort, + Operator, Rating, TagSort, UpdateMode, @@ -72,6 +73,10 @@ export const ComicSortLabel: Record<ComicSort, string> = { [ComicSort.CreatedAt]: 'Created At', [ComicSort.UpdatedAt]: 'Updated At', [ComicSort.TagCount]: 'Tag Count', + [ComicSort.ArtistCount]: 'Artist Count', + [ComicSort.CharacterCount]: 'Character Count', + [ComicSort.CircleCount]: 'Circle Count', + [ComicSort.WorldCount]: 'World Count', [ComicSort.PageCount]: 'Page Count', [ComicSort.Random]: 'Random' }; @@ -80,21 +85,24 @@ export const ArtistSortLabel: Record<ArtistSort, string> = { [ArtistSort.Name]: 'Name', [ArtistSort.CreatedAt]: 'Created At', [ArtistSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [ArtistSort.ComicCount]: 'Comic Count', + [ArtistSort.Random]: 'Random' }; export const CharacterSortLabel: Record<CharacterSort, string> = { [CharacterSort.Name]: 'Name', [CharacterSort.CreatedAt]: 'Created At', [CharacterSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [CharacterSort.ComicCount]: 'Comic Count', + [CharacterSort.Random]: 'Random' }; export const CircleSortLabel: Record<CircleSort, string> = { [CircleSort.Name]: 'Name', [CircleSort.CreatedAt]: 'Created At', [CircleSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [CircleSort.ComicCount]: 'Comic Count', + [CircleSort.Random]: 'Random' }; export const NamespaceSortLabel: Record<NamespaceSort, string> = { @@ -102,21 +110,25 @@ export const NamespaceSortLabel: Record<NamespaceSort, string> = { [NamespaceSort.SortName]: 'Sort Name', [NamespaceSort.CreatedAt]: 'Created At', [NamespaceSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [NamespaceSort.TagCount]: 'Tag Count', + [NamespaceSort.Random]: 'Random' }; export const TagSortLabel: Record<TagSort, string> = { [TagSort.Name]: 'Name', [TagSort.CreatedAt]: 'Created At', + [TagSort.ComicCount]: 'Comic Count', + [TagSort.NamespaceCount]: 'Namespace Count', [TagSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [TagSort.Random]: 'Random' }; export const WorldSortLabel: Record<WorldSort, string> = { [WorldSort.Name]: 'Name', [WorldSort.CreatedAt]: 'Created At', [WorldSort.UpdatedAt]: 'Updated At', - [ArchiveSort.Random]: 'Random' + [WorldSort.ComicCount]: 'Comic Count', + [WorldSort.Random]: 'Random' }; export const UpdateModeLabel: Record<UpdateMode, string> = { @@ -125,6 +137,12 @@ export const UpdateModeLabel: Record<UpdateMode, string> = { [UpdateMode.Replace]: 'Replace' }; +export const OperatorLabel: Record<Operator, string> = { + [Operator.Equal]: 'Equal', + [Operator.GreaterThan]: 'Greater than', + [Operator.LowerThan]: 'Lower than,' +}; + export const LanguageLabel: Record<Language, string> = { [Language.Ab]: 'Abkhazian', [Language.Aa]: 'Afar', diff --git a/frontend/src/lib/Filter.svelte.ts b/frontend/src/lib/Filter.svelte.ts index 6183f06..390b98a 100644 --- a/frontend/src/lib/Filter.svelte.ts +++ b/frontend/src/lib/Filter.svelte.ts @@ -1,8 +1,11 @@ import { + Operator, type ArchiveFilter, type ArchiveFilterInput, type ComicFilter, type ComicFilterInput, + type NamespaceFilter, + type NamespaceFilterInput, type StringFilter, type TagFilter, type TagFilterInput @@ -15,10 +18,18 @@ interface FilterInput<T> { exclude?: T | null; } -interface BasicFilter { +interface NameFilter { name?: { contains?: string | null } | null; } +interface AssociationCount { + count: { value: number; operator?: Operator | null }; +} + +interface BasicFilter extends NameFilter { + comics?: AssociationCount | null; +} + export type FilterType = 'include' | 'exclude'; type FilterMode = 'any' | 'all' | 'exact'; @@ -30,7 +41,7 @@ type AssocFilter<T, K extends Key> = Filter< any?: T[] | null; all?: T[] | null; exact?: T[] | null; - empty?: boolean | null; + count?: { value: number; operator?: Operator | null } | null; }, K >; @@ -62,10 +73,6 @@ class ComplexMember<K extends Key> { if (this.values.length > 0) { filter[this.key] = { [this.mode]: this.values }; } - - if (this.empty) { - filter[this.key] = { ...filter[this.key], empty: this.empty }; - } } } @@ -80,7 +87,9 @@ export class Association<K extends Key> extends ComplexMember<K> { } const prop = filter[key]; - this.empty = prop?.empty; + this.empty = + prop?.count?.value === 0 && + (prop.count.operator === undefined || prop.count.operator === Operator.Equal); if (prop?.all && prop.all.length > 0) { this.mode = 'all'; @@ -93,6 +102,13 @@ export class Association<K extends Key> extends ComplexMember<K> { this.values = prop.exact; } } + + integrate(filter: AssocFilter<unknown, K>) { + super.integrate(filter); + if (this.empty) { + filter[this.key] = { ...filter[this.key], count: { value: 0, operator: Operator.Equal } }; + } + } } export class Enum<K extends Key> extends ComplexMember<K> { @@ -112,6 +128,13 @@ export class Enum<K extends Key> extends ComplexMember<K> { this.values = prop.any; } } + + integrate(filter: EnumFilter<K>) { + super.integrate(filter); + if (this.empty) { + filter[this.key] = { ...filter[this.key], empty: this.empty }; + } + } } class Bool<K extends Key> { @@ -133,6 +156,27 @@ class Bool<K extends Key> { } } +class Orphan<K extends Key> { + key: K; + value?: boolean = false; + + constructor(key: K, filter?: Filter<AssociationCount, K> | null) { + this.key = key; + + if (filter) { + this.value = + filter[key]?.count?.value === 0 && + (filter[key].count.operator === undefined || filter[key].count.operator === Operator.Equal); + } + } + + integrate(filter: Filter<AssociationCount, K>) { + if (this.value) { + filter[this.key] = { count: { value: 0, operator: Operator.Equal } }; + } + } +} + class Str<K extends Key> { key: K; contains = $state(''); @@ -164,7 +208,7 @@ export class ArchiveFilterControls extends Controls<ArchiveFilter> { path: Str<'path'>; organized: Bool<'organized'>; - constructor(filter: ArchiveFilter | null | undefined) { + constructor(filter?: ArchiveFilter | null) { super(); this.path = new Str('path', filter); @@ -210,16 +254,26 @@ export class ComicFilterControls extends Controls<ComicFilter> { } } -export class BasicFilterControls extends Controls<BasicFilter> { +export class NameFilterControls extends Controls<NameFilter> { name: Str<'name'>; - constructor(filter?: BasicFilter | null) { + constructor(filter?: NameFilter | null) { super(); this.name = new Str('name', filter); } } +export class BasicFilterControls extends NameFilterControls { + orphan: Orphan<'comics'>; + + constructor(filter?: BasicFilter | null) { + super(filter); + + this.orphan = new Orphan('comics', filter); + } +} + export class TagFilterControls extends BasicFilterControls { namespaces: Association<'namespaces'>; @@ -230,6 +284,16 @@ export class TagFilterControls extends BasicFilterControls { } } +export class NamespaceFilterControls extends NameFilterControls { + orphan: Orphan<'tags'>; + + constructor(filter?: NamespaceFilter | null) { + super(filter); + + this.orphan = new Orphan('tags', filter); + } +} + function buildFilterInput<F>(include?: F, exclude?: F) { const input: FilterInput<F> = {}; @@ -305,7 +369,7 @@ export class BasicFilterContext extends FilterContext<BasicFilter> { export class TagFilterContext extends FilterContext<TagFilter> { include: TagFilterControls; exclude: TagFilterControls; - private static ignore = ['name']; + private static ignore = ['name', 'comics']; constructor(filter: TagFilterInput) { super(); @@ -317,6 +381,18 @@ export class TagFilterContext extends FilterContext<TagFilter> { } } +export class NamespaceFilterContext extends FilterContext<NamespaceFilter> { + include: NamespaceFilterControls; + exclude: NamespaceFilterControls; + + constructor(filter: NamespaceFilterInput) { + super(); + + this.include = new NamespaceFilterControls(filter.include); + this.exclude = new NamespaceFilterControls(); + } +} + export function cycleBooleanFilter(value: boolean | undefined, tristate = true) { if (tristate) { if (value === undefined) { diff --git a/frontend/src/lib/Shortcuts.ts b/frontend/src/lib/Shortcuts.ts index 1ff7679..82f19ac 100644 --- a/frontend/src/lib/Shortcuts.ts +++ b/frontend/src/lib/Shortcuts.ts @@ -31,7 +31,7 @@ type UppercaseLetter = Uppercase<LowercaseLetter>; type Letter = LowercaseLetter | UppercaseLetter; type Special = '?' | 'Enter' | 'Escape' | 'Delete'; -const modeSwitches = ['n', 'g', 'i'] as const; +const modeSwitches = ['n', 'g', 'i', 'e'] as const; type ModeSwitch = (typeof modeSwitches)[number]; function isModeSwitch(s: string): s is ModeSwitch { diff --git a/frontend/src/lib/components/ArchiveCard.svelte b/frontend/src/lib/components/ArchiveCard.svelte new file mode 100644 index 0000000..c9d283b --- /dev/null +++ b/frontend/src/lib/components/ArchiveCard.svelte @@ -0,0 +1,39 @@ +<script lang="ts"> + import type { ArchiveFragment } from '$gql/graphql'; + import FooterPill from '$lib/pills/FooterPill.svelte'; + import { filesize } from 'filesize'; + import { type Snippet } from 'svelte'; + import Card from './Card.svelte'; + + interface Props { + archive: ArchiveFragment; + overlay?: Snippet; + onclick?: (event: MouseEvent) => void; + } + + let { archive, overlay, onclick }: Props = $props(); + + let details = $derived({ + title: archive.name, + cover: archive.cover + }); + let href = $derived(`/archives/${archive.id.toString()}`); +</script> + +<Card {details} {href} {onclick} {overlay}> + {#snippet footer()} + <div class="flex flex-wrap gap-1"> + <FooterPill text={`${archive.pageCount} pages`}> + {#snippet icon()} + <span class="icon-[material-symbols--description] mr-0.5 text-sm"></span> + {/snippet} + </FooterPill> + <div class="flex grow"></div> + <FooterPill text={filesize(archive.size, { base: 2 })}> + {#snippet icon()} + <span class="icon-[material-symbols--hard-drive] mr-0.5 text-sm"></span> + {/snippet} + </FooterPill> + </div> + {/snippet} +</Card> diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte index b8e8ecf..8cfe34d 100644 --- a/frontend/src/lib/components/Card.svelte +++ b/frontend/src/lib/components/Card.svelte @@ -1,5 +1,8 @@ -<script lang="ts" module> - import type { ComicFragment, ImageFragment } from '$gql/graphql'; +<script lang="ts"> + import type { ImageFragment } from '$gql/graphql'; + import { src } from '$lib/Utils'; + import Star from '$lib/icons/Star.svelte'; + import type { Snippet } from 'svelte'; interface CardDetails { title: string; @@ -8,24 +11,6 @@ cover?: ImageFragment; } - export function comicCard(comic: ComicFragment) { - return { - href: `/comics/${comic.id.toString()}`, - details: { - title: comic.title, - subtitle: comic.originalTitle, - favourite: comic.favourite, - cover: comic.cover - } - }; - } -</script> - -<script lang="ts"> - import { src } from '$lib/Utils'; - import Star from '$lib/icons/Star.svelte'; - import type { Snippet } from 'svelte'; - interface Props { href: string; details: CardDetails; @@ -33,6 +18,7 @@ coverOnly?: boolean; overlay?: Snippet; children?: Snippet; + footer?: Snippet; onclick?: (event: MouseEvent) => void; } @@ -43,6 +29,7 @@ coverOnly = false, overlay, children, + footer, onclick }: Props = $props(); </script> @@ -66,8 +53,8 @@ /> {/if} {#if !coverOnly} - <article class="flex h-full flex-col gap-2 p-2"> - <header> + <article class="p flex h-full flex-col p-2 pb-1"> + <header class="mb-2"> <h2 class="self-center text-sm font-medium [grid-area:title]" title={details.title}> {details.title} </h2> @@ -86,9 +73,15 @@ {/if} </header> - <section class="max-h-full grow overflow-auto border-t border-slate-800/80 pt-2 text-xs"> + <section class="max-h-full grow overflow-auto border-y border-slate-800/80 pt-2 text-xs"> {@render children?.()} </section> + + {#if footer} + <div class="mt-1 text-xs"> + {@render footer()} + </div> + {/if} </article> {/if} </a> diff --git a/frontend/src/lib/components/ComicCard.svelte b/frontend/src/lib/components/ComicCard.svelte new file mode 100644 index 0000000..cb73e97 --- /dev/null +++ b/frontend/src/lib/components/ComicCard.svelte @@ -0,0 +1,75 @@ +<script lang="ts"> + import type { ComicFragment } from '$gql/graphql'; + import AssociationPill from '$lib/pills/AssociationPill.svelte'; + import FooterPill from '$lib/pills/FooterPill.svelte'; + import TagPill from '$lib/pills/TagPill.svelte'; + import { type Snippet } from 'svelte'; + import Card from './Card.svelte'; + + interface Props { + comic: ComicFragment; + overlay?: Snippet; + compact?: boolean; + coverOnly?: boolean; + onclick?: (event: MouseEvent) => void; + } + + let { comic, overlay, compact, coverOnly, onclick }: Props = $props(); + + let details = $derived({ + title: comic.title, + subtitle: comic.originalTitle, + favourite: comic.favourite, + cover: comic.cover + }); + let href = $derived(`/comics/${comic.id.toString()}`); +</script> + +<Card {details} {href} {compact} {onclick} {overlay} {coverOnly}> + <div class="flex flex-col gap-1"> + {#if comic.artists.length || comic.circles.length} + <div class="flex flex-wrap gap-1"> + {#each comic.artists as { name } (name)} + <AssociationPill {name} type="artist" /> + {/each} + {#each comic.circles as { name } (name)} + <AssociationPill {name} type="circle" /> + {/each} + </div> + {/if} + {#if comic.characters.length || comic.worlds.length} + <div class="flex flex-wrap gap-1"> + {#each comic.worlds as { name } (name)} + <AssociationPill {name} type="world" /> + {/each} + {#each comic.characters as { name } (name)} + <AssociationPill {name} type="character" /> + {/each} + </div> + {/if} + {#if comic.tags.length} + <div class="flex flex-wrap gap-1"> + {#each comic.tags as { name, description } (name)} + <TagPill {name} {description} /> + {/each} + </div> + {/if} + </div> + {#snippet footer()} + <div class="flex flex-wrap gap-1"> + <FooterPill text={`${comic.pageCount} pages`}> + {#snippet icon()} + <span class="icon-[material-symbols--description] mr-0.5 text-sm"></span> + {/snippet} + </FooterPill> + <div class="flex grow"></div> + {#if comic.date} + <FooterPill text={comic.date}> + {#snippet icon()} + <span class="icon-[material-symbols--calendar-today] mr-0.5 text-sm"></span> + {/snippet} + </FooterPill> + {/if} + </div> + {/snippet} +</Card> diff --git a/frontend/src/lib/components/Expander.svelte b/frontend/src/lib/components/Expander.svelte deleted file mode 100644 index 8f23042..0000000 --- a/frontend/src/lib/components/Expander.svelte +++ /dev/null @@ -1,21 +0,0 @@ -<script lang="ts"> - interface Props { - expanded: boolean; - title: string; - } - - let { expanded = $bindable(), title }: Props = $props(); - - function onclick() { - expanded = !expanded; - } -</script> - -<button class="flex items-center text-base hover:text-white" type="button" {onclick}> - {#if expanded} - <span class="icon-base icon-[material-symbols--expand-less]"></span> - {:else} - <span class="icon-base icon-[material-symbols--expand-more]"></span> - {/if} - {title} -</button> diff --git a/frontend/src/lib/filter/ComicFilterForm.svelte b/frontend/src/lib/filter/ComicFilterForm.svelte index 277edd1..4c6e2d2 100644 --- a/frontend/src/lib/filter/ComicFilterForm.svelte +++ b/frontend/src/lib/filter/ComicFilterForm.svelte @@ -2,6 +2,7 @@ import { artistList, characterList, circleList, comicTagList, worldList } from '$gql/Queries'; import { categories, censorships, languages, ratings } from '$lib/Enums'; import { ComicFilterContext } from '$lib/Filter.svelte'; + import { accelerator } from '$lib/Shortcuts'; import { getContextClient } from '@urql/svelte'; import Filter from './components/Filter.svelte'; import FilterForm from './components/FilterForm.svelte'; @@ -21,38 +22,42 @@ let characters = $derived($charactersQuery.data?.characters.edges); let circles = $derived($circlesQuery.data?.circles.edges); let worlds = $derived($worldsQuery.data?.worlds.edges); + + let inc = $derived(filter.include); + let exc = $derived(filter.exclude); </script> -<FilterForm type="grid" apply={filter.apply} expanded={filter.excludes > 0}> +<FilterForm type="grid" apply={filter.apply}> {#snippet include(type)} - <Filter {type} wide title="Tags" options={tags} filter={filter.include.tags} /> - <Filter {type} title="Artists" options={artists} filter={filter.include.artists} /> - <Filter {type} title="Circles" options={circles} filter={filter.include.circles} /> - <Filter {type} title="Characters" options={characters} filter={filter.include.characters} /> - <Filter {type} title="Worlds" options={worlds} filter={filter.include.worlds} /> - <Filter {type} title="Categories" options={categories} filter={filter.include.categories} /> - <Filter {type} title="Ratings" options={ratings} filter={filter.include.ratings} /> - <Filter {type} title="Censorship" options={censorships} filter={filter.include.censorships} /> - <Filter {type} title="Languages" options={languages} filter={filter.include.languages} /> + <Filter {type} wide title="Tags" options={tags} filter={inc.tags} accel="it" /> + <Filter {type} title="Artists" options={artists} filter={inc.artists} accel="ia" /> + <Filter {type} title="Circles" options={circles} filter={inc.circles} accel="ii" /> + <Filter {type} title="Characters" options={characters} filter={inc.characters} accel="ih" /> + <Filter {type} title="Worlds" options={worlds} filter={inc.worlds} accel="iw" /> + <Filter {type} title="Categories" options={categories} filter={inc.categories} accel="ig" /> + <Filter {type} title="Ratings" options={ratings} filter={inc.ratings} accel="ir" /> + <Filter {type} title="Censorship" options={censorships} filter={inc.censorships} accel="is" /> + <Filter {type} title="Languages" options={languages} filter={inc.languages} accel="il" /> <div class="flex flex-col"> <label for="include-url">URL</label> <input + use:accelerator={'iu'} id="include-url" class="h-full" placeholder="Search..." - bind:value={filter.include.url.contains} + bind:value={inc.url.contains} /> </div> {/snippet} {#snippet exclude(type)} - <Filter {type} wide title="Tags" options={tags} filter={filter.exclude.tags} /> - <Filter {type} title="Artists" options={artists} filter={filter.exclude.artists} /> - <Filter {type} title="Circles" options={circles} filter={filter.exclude.circles} /> - <Filter {type} title="Characters" options={characters} filter={filter.exclude.characters} /> - <Filter {type} title="Worlds" options={worlds} filter={filter.exclude.worlds} /> - <Filter {type} title="Categories" options={categories} filter={filter.exclude.categories} /> - <Filter {type} title="Ratings" options={ratings} filter={filter.exclude.ratings} /> - <Filter {type} title="Censorship" options={censorships} filter={filter.exclude.censorships} /> - <Filter {type} title="Languages" options={languages} filter={filter.exclude.languages} /> + <Filter {type} wide title="Tags" options={tags} filter={exc.tags} accel="et" /> + <Filter {type} title="Artists" options={artists} filter={exc.artists} accel="ea" /> + <Filter {type} title="Circles" options={circles} filter={exc.circles} accel="ei" /> + <Filter {type} title="Characters" options={characters} filter={exc.characters} accel="eh" /> + <Filter {type} title="Worlds" options={worlds} filter={exc.worlds} accel="ew" /> + <Filter {type} title="Categories" options={categories} filter={exc.categories} accel="eg" /> + <Filter {type} title="Ratings" options={ratings} filter={exc.ratings} accel="er" /> + <Filter {type} title="Censorship" options={censorships} filter={exc.censorships} accel="es" /> + <Filter {type} title="Languages" options={languages} filter={exc.languages} accel="el" /> {/snippet} </FilterForm> diff --git a/frontend/src/lib/filter/TagFilterForm.svelte b/frontend/src/lib/filter/TagFilterForm.svelte index 280db8a..c514163 100644 --- a/frontend/src/lib/filter/TagFilterForm.svelte +++ b/frontend/src/lib/filter/TagFilterForm.svelte @@ -11,13 +11,16 @@ let namespaceQuery = $derived(namespaceList(client)); let namespaces = $derived($namespaceQuery.data?.namespaces.edges); + + let inc = $derived(filter.include); + let exc = $derived(filter.exclude); </script> -<FilterForm apply={filter.apply} expanded={filter.excludes > 0}> +<FilterForm apply={filter.apply}> {#snippet include(type)} - <Filter {type} title="Namespaces" options={namespaces} filter={filter.include.namespaces} /> + <Filter {type} title="Namespaces" options={namespaces} filter={inc.namespaces} accel="in" /> {/snippet} {#snippet exclude(type)} - <Filter {type} title="Namespaces" options={namespaces} filter={filter.exclude.namespaces} /> + <Filter {type} title="Namespaces" options={namespaces} filter={exc.namespaces} accel="en" /> {/snippet} </FilterForm> diff --git a/frontend/src/lib/filter/components/Filter.svelte b/frontend/src/lib/filter/components/Filter.svelte index 832ac19..8264771 100644 --- a/frontend/src/lib/filter/components/Filter.svelte +++ b/frontend/src/lib/filter/components/Filter.svelte @@ -1,5 +1,6 @@ <script lang="ts"> import { Association, Enum, type FilterType } from '$lib/Filter.svelte'; + import { accelerator, type Shortcut } from '$lib/Shortcuts'; import type { ListItem } from '$lib/Utils'; import Select from '$lib/components/Select.svelte'; @@ -8,18 +9,20 @@ type: FilterType; options: ListItem[] | undefined; filter: Association<string> | Enum<string>; + accel: Shortcut; wide?: boolean; } - let { title, type, options, filter, wide = false }: Props = $props(); + let { title, type, options, filter, accel, wide = false }: Props = $props(); let exclude = $derived(type === 'exclude'); + let placeholder = $derived(exclude ? 'Exclude...' : 'Include...'); const id = `${type}-${title.toLowerCase()}`; </script> <div class:exclude class:wide class="[&.wide]:col-span-2"> <div class="flex gap-2"> - <label for={id}>{title}</label> + <label use:accelerator={accel} for={id}>{title}</label> <div class="ml-auto flex items-center gap-1 self-center text-xs"> {#if filter instanceof Association} <button @@ -62,5 +65,5 @@ </button> </div> </div> - <Select multi clearable {options} {id} bind:value={filter.values} /> + <Select multi clearable {placeholder} {options} {id} bind:value={filter.values} /> </div> diff --git a/frontend/src/lib/filter/components/FilterForm.svelte b/frontend/src/lib/filter/components/FilterForm.svelte index a32faf8..717a56d 100644 --- a/frontend/src/lib/filter/components/FilterForm.svelte +++ b/frontend/src/lib/filter/components/FilterForm.svelte @@ -1,6 +1,5 @@ <script lang="ts"> import { page } from '$app/state'; - import Expander from '$lib/components/Expander.svelte'; import type { FilterType } from '$lib/Filter.svelte'; import type { Snippet } from 'svelte'; @@ -8,13 +7,10 @@ type?: 'grid' | 'row'; include?: Snippet<[FilterType]>; exclude?: Snippet<[FilterType]>; - expanded: boolean; apply: (params: URLSearchParams) => void; } - let { type = 'row', include, exclude, expanded: initialExpanded, apply }: Props = $props(); - - let expanded = $state(initialExpanded); + let { type = 'row', include, exclude, apply }: Props = $props(); function onsubmit(event: SubmitEvent) { event.preventDefault(); @@ -22,21 +18,17 @@ } </script> -<form {onsubmit} class="gap-0"> +<form {onsubmit} class="gap-4"> {#if type === 'grid'} <div class="flex flex-col gap-4 px-2 md:grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"> {@render include?.('include')} </div> - <div class="my-2 flex justify-start"> - <Expander title="Exclude" bind:expanded /> + + <div + class="flex flex-col gap-4 bg-rose-950/50 p-2 md:grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6" + > + {@render exclude?.('exclude')} </div> - {#if expanded} - <div - class="flex flex-col gap-4 bg-rose-950/50 p-2 md:grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6" - > - {@render exclude?.('exclude')} - </div> - {/if} {:else} <div class="flex flex-wrap justify-center gap-2 *:basis-full xl:*:basis-1/3 2xl:*:basis-1/5"> <div class="p-2"> diff --git a/frontend/src/lib/icons/Orphan.svelte b/frontend/src/lib/icons/Orphan.svelte new file mode 100644 index 0000000..7d947d2 --- /dev/null +++ b/frontend/src/lib/icons/Orphan.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + interface Props { + orphaned?: boolean; + hoverable?: boolean; + } + + let { orphaned, hoverable = false }: Props = $props(); +</script> + +{#if orphaned} + <span class:hoverable class="icon-gray icon-base icon-[material-symbols--fmd-bad]"></span> +{:else} + <span class:hoverable class="icon-gray icon-base dim icon-[material-symbols--fmd-bad-outline]" + ></span> +{/if} diff --git a/frontend/src/lib/pills/ComicPills.svelte b/frontend/src/lib/pills/ComicPills.svelte deleted file mode 100644 index 45c42fd..0000000 --- a/frontend/src/lib/pills/ComicPills.svelte +++ /dev/null @@ -1,37 +0,0 @@ -<script lang="ts"> - import type { ComicFragment } from '$gql/graphql'; - import AssociationPill from '$lib/pills/AssociationPill.svelte'; - import TagPill from '$lib/pills/TagPill.svelte'; - - let { comic }: { comic: ComicFragment } = $props(); -</script> - -<div class="flex flex-col gap-1"> - {#if comic.artists.length || comic.circles.length} - <div class="flex flex-wrap gap-1"> - {#each comic.artists as { name } (name)} - <AssociationPill {name} type="artist" /> - {/each} - {#each comic.circles as { name } (name)} - <AssociationPill {name} type="circle" /> - {/each} - </div> - {/if} - {#if comic.characters.length || comic.worlds.length} - <div class="flex flex-wrap gap-1"> - {#each comic.worlds as { name } (name)} - <AssociationPill {name} type="world" /> - {/each} - {#each comic.characters as { name } (name)} - <AssociationPill {name} type="character" /> - {/each} - </div> - {/if} - {#if comic.tags.length} - <div class="flex flex-wrap gap-1"> - {#each comic.tags as { name, description } (name)} - <TagPill {name} {description} /> - {/each} - </div> - {/if} -</div> diff --git a/frontend/src/lib/pills/FooterPill.svelte b/frontend/src/lib/pills/FooterPill.svelte new file mode 100644 index 0000000..3da1811 --- /dev/null +++ b/frontend/src/lib/pills/FooterPill.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + import type { Snippet } from 'svelte'; + + interface Props { + text: string; + icon?: Snippet; + } + + let { text, icon }: Props = $props(); +</script> + +<div class="flex items-center rounded-sm p-0.5 text-zinc-300"> + {@render icon?.()} + <span>{text}</span> +</div> diff --git a/frontend/src/lib/reader/Reader.svelte b/frontend/src/lib/reader/Reader.svelte index 2e7e851..0665698 100644 --- a/frontend/src/lib/reader/Reader.svelte +++ b/frontend/src/lib/reader/Reader.svelte @@ -6,10 +6,13 @@ import { fade, slide } from 'svelte/transition'; import CloseReaderButton from './components/CloseReaderButton.svelte'; import ReaderMenuButton from './components/ReaderMenuButton.svelte'; + import ToggleFullscreenButton from './components/ToggleFullscreenButton.svelte'; let { sidebar, children }: { sidebar?: Snippet; children?: Snippet } = $props(); const reader = getReaderContext(); + + let dialog: HTMLDivElement | undefined = $state(); </script> {#if reader.visible} @@ -18,6 +21,7 @@ class="fixed top-0 right-0 bottom-0 left-0 z-10 flex h-full w-full bg-black" transition:fade={fadeDefault} use:trapFocus + bind:this={dialog} > {#if sidebar && reader.sidebar} <aside class="w-[36rem] shrink-0 bg-slate-800" transition:slide={slideXDefault}> @@ -27,11 +31,16 @@ </aside> {/if} <main class="relative flex grow"> - <div class="absolute flex w-full p-1 text-lg [&>*:last-child]:ml-auto"> - {#if sidebar} - <ReaderMenuButton /> - {/if} - <CloseReaderButton /> + <div class="absolute flex w-full p-1 text-lg"> + <div class="flex flex-col gap-1"> + {#if sidebar} + <ReaderMenuButton /> + {/if} + </div> + <div class="ml-auto flex flex-col gap-1"> + <CloseReaderButton /> + <ToggleFullscreenButton {dialog} /> + </div> </div> {@render children?.()} diff --git a/frontend/src/lib/reader/components/ReaderMenuButton.svelte b/frontend/src/lib/reader/components/ReaderMenuButton.svelte index da494e3..924342f 100644 --- a/frontend/src/lib/reader/components/ReaderMenuButton.svelte +++ b/frontend/src/lib/reader/components/ReaderMenuButton.svelte @@ -9,7 +9,7 @@ <button type="button" - class="btn-transparent invisible xl:visible" + class="btn-transparent hidden xl:flex" {title} aria-label={title} onclick={() => (reader.sidebar = !reader.sidebar)} diff --git a/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte b/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte new file mode 100644 index 0000000..9ad4ce6 --- /dev/null +++ b/frontend/src/lib/reader/components/ToggleFullscreenButton.svelte @@ -0,0 +1,34 @@ +<script lang="ts"> + import { accelerator } from '$lib/Shortcuts'; + import { toastFinally } from '$lib/Toasts'; + + let { dialog }: { dialog?: HTMLElement } = $props(); + + function onclick() { + if (isFullscreen) { + document.exitFullscreen().catch(toastFinally); + } else if (dialog?.requestFullscreen) { + dialog.requestFullscreen().catch(toastFinally); + } + } + + let fullscreenElement: HTMLElement | null = $state(null); + let isFullscreen = $derived(fullscreenElement !== null); +</script> + +<svelte:document bind:fullscreenElement /> + +<button + type="button" + class="btn-transparent" + title="Toggle fullscreen" + aria-label="Toggle fullscreen" + {onclick} + use:accelerator={'f'} +> + {#if isFullscreen} + <span class="icon-lg icon-[material-symbols--fullscreen-exit]"></span> + {:else} + <span class="icon-lg icon-[material-symbols--fullscreen]"></span> + {/if} +</button> diff --git a/frontend/src/lib/tabs/ArchiveDetails.svelte b/frontend/src/lib/tabs/ArchiveDetails.svelte index b3d570f..1243162 100644 --- a/frontend/src/lib/tabs/ArchiveDetails.svelte +++ b/frontend/src/lib/tabs/ArchiveDetails.svelte @@ -1,8 +1,7 @@ <script lang="ts"> import type { FullArchiveFragment } from '$gql/graphql'; import { formatListSize, joinText } from '$lib/Utils'; - import Card, { comicCard } from '$lib/components/Card.svelte'; - import ComicPills from '$lib/pills/ComicPills.svelte'; + import ComicCard from '$lib/components/ComicCard.svelte'; import { formatDistance, formatISO9075 } from 'date-fns'; import { filesize } from 'filesize'; import Header from './DetailsHeader.svelte'; @@ -40,9 +39,7 @@ <h2 class="text-base font-medium">Comics</h2> <div class="flex shrink-0 flex-col gap-4"> {#each archive.comics as comic} - <Card compact {...comicCard(comic)}> - <ComicPills {comic} /> - </Card> + <ComicCard compact {comic} /> {/each} </div> </div> diff --git a/frontend/src/lib/tabs/ArchiveEdit.svelte b/frontend/src/lib/tabs/ArchiveEdit.svelte index 83a492b..2ed0523 100644 --- a/frontend/src/lib/tabs/ArchiveEdit.svelte +++ b/frontend/src/lib/tabs/ArchiveEdit.svelte @@ -3,9 +3,8 @@ import { type FullArchiveFragment } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; - import Card, { comicCard } from '$lib/components/Card.svelte'; + import ComicCard from '$lib/components/ComicCard.svelte'; import OrganizedButton from '$lib/components/OrganizedButton.svelte'; - import ComicPills from '$lib/pills/ComicPills.svelte'; import { getSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import { getContextClient } from '@urql/svelte'; @@ -57,12 +56,11 @@ <h2 class="text-base font-medium">Comics</h2> <div class="flex shrink-0 flex-col gap-4"> {#each archive.comics as comic} - <Card compact {...comicCard(comic)}> + <ComicCard compact {comic}> {#snippet overlay()} <AddOverlay id={comic.id} /> {/snippet} - <ComicPills {comic} /> - </Card> + </ComicCard> {/each} </div> </div> diff --git a/frontend/src/lib/toolbar/FilterOrganized.svelte b/frontend/src/lib/toolbar/FilterOrganized.svelte index 0f95e5f..9fc9d21 100644 --- a/frontend/src/lib/toolbar/FilterOrganized.svelte +++ b/frontend/src/lib/toolbar/FilterOrganized.svelte @@ -24,7 +24,7 @@ class="btn-slate" title="Filter organized" onclick={toggle} - use:accelerator={'o'} + use:accelerator={'z'} > <Organized tristate {organized} /> </button> diff --git a/frontend/src/lib/toolbar/FilterOrphaned.svelte b/frontend/src/lib/toolbar/FilterOrphaned.svelte new file mode 100644 index 0000000..7e79be1 --- /dev/null +++ b/frontend/src/lib/toolbar/FilterOrphaned.svelte @@ -0,0 +1,24 @@ +<script lang="ts"> + import { page } from '$app/state'; + import { BasicFilterContext, NamespaceFilterContext } from '$lib/Filter.svelte'; + import { accelerator } from '$lib/Shortcuts'; + import Orphan from '$lib/icons/Orphan.svelte'; + + let { filter }: { filter: BasicFilterContext | NamespaceFilterContext } = $props(); + let orphaned = $derived(filter.include.orphan.value); + + const toggle = () => { + filter.include.orphan.value = !orphaned; + filter.apply(page.url.searchParams); + }; +</script> + +<button + class:toggled={orphaned} + class="btn-slate" + title="Filter orphaned" + onclick={toggle} + use:accelerator={'r'} +> + <Orphan {orphaned} /> +</button> diff --git a/frontend/src/lib/toolbar/Search.svelte b/frontend/src/lib/toolbar/Search.svelte index 4806971..d5971bc 100644 --- a/frontend/src/lib/toolbar/Search.svelte +++ b/frontend/src/lib/toolbar/Search.svelte @@ -19,5 +19,5 @@ placeholder="Search {name}..." bind:value={field} use:debounce={{ callback: () => filter.apply(page.url.searchParams) }} - use:accelerator={'F'} + use:accelerator={'q'} /> diff --git a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte index ee07902..2ef63f4 100644 --- a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte +++ b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { page } from '$app/state'; import { navigate } from '$lib/Navigation'; + import { accelerator } from '$lib/Shortcuts'; import { slideXFast } from '$lib/Transitions'; import Badge from '$lib/components/Badge.svelte'; import { slide } from 'svelte/transition'; @@ -19,6 +20,7 @@ class="btn-slate relative" title={`${expanded ? 'Hide' : 'Show'} filters`} onclick={toggle} + use:accelerator={'F'} > {#if expanded} <span class="icon-base icon-[material-symbols--filter-alt]"></span> @@ -34,6 +36,7 @@ transition:slide={slideXFast} title="Reset filters" aria-label="Reset filters" + use:accelerator={'X'} > <div class="flex"> <span class="icon-base icon-[material-symbols--filter-alt-off]"></span> diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index d2a59c8..237b573 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -6,7 +6,7 @@ import { href } from '$lib/Navigation'; import { fadeDefault } from '$lib/Transitions'; import logo from '$lib/assets/logo.webp'; - import Card, { comicCard } from '$lib/components/Card.svelte'; + import ComicCard from '$lib/components/ComicCard.svelte'; import Guard from '$lib/components/Guard.svelte'; import Head from '$lib/components/Head.svelte'; import Carousel from '$lib/containers/Carousel.svelte'; @@ -52,21 +52,21 @@ {#if recent && recent.count > 0} <Carousel title="Recently added" href={recentLink}> {#each recent.edges as comic} - <Card coverOnly {...comicCard(comic)} /> + <ComicCard coverOnly {comic} /> {/each} </Carousel> {/if} {#if favourites && favourites.count > 0} <Carousel title="Favourites" href={favouriteLink}> {#each favourites.edges as comic} - <Card coverOnly {...comicCard(comic)} /> + <ComicCard coverOnly {comic} /> {/each} </Carousel> {/if} {#if bookmarked && bookmarked.count > 0} <Carousel title="Bookmarks" href={bookmarkLink}> {#each bookmarked.edges as comic} - <Card coverOnly {...comicCard(comic)} /> + <ComicCard coverOnly {comic} /> {/each} </Carousel> {/if} diff --git a/frontend/src/routes/archives/+page.svelte b/frontend/src/routes/archives/+page.svelte index b75c140..2bc6703 100644 --- a/frontend/src/routes/archives/+page.svelte +++ b/frontend/src/routes/archives/+page.svelte @@ -4,7 +4,7 @@ import type { ArchiveFragment } from '$gql/graphql'; import { ArchiveSortLabel } from '$lib/Enums'; import { ArchiveFilterContext } from '$lib/Filter.svelte'; - import Card from '$lib/components/Card.svelte'; + import ArchiveCard from '$lib/components/ArchiveCard.svelte'; import Empty from '$lib/components/Empty.svelte'; import Guard from '$lib/components/Guard.svelte'; import Head from '$lib/components/Head.svelte'; @@ -12,7 +12,6 @@ import Cards from '$lib/containers/Cards.svelte'; import Column from '$lib/containers/Column.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; - import Pill from '$lib/pills/Pill.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; @@ -26,7 +25,6 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { filesize } from 'filesize'; import type { PageProps } from './$types'; let { data }: PageProps = $props(); @@ -79,26 +77,14 @@ <Pagination {pagination} total={archives.count} /> <main> <Cards> - {#each archives.edges as { id, name, cover, size, pageCount }, index (id)} - <Selectable {index} {id}> + {#each archives.edges as archive, index (archive.id)} + <Selectable {index} id={archive.id}> {#snippet children({ onclick, selected })} - <Card href={id.toString()} details={{ title: name, cover }} {onclick}> + <ArchiveCard {archive} {onclick}> {#snippet overlay()} <SelectionOverlay position="left" {selected} /> {/snippet} - <div class="flex gap-1 text-xs"> - <Pill name={`${pageCount} pages`}> - {#snippet icon()} - <span class="icon-[material-symbols--note] mr-0.5"></span> - {/snippet} - </Pill> - <Pill name={filesize(size, { base: 2 })}> - {#snippet icon()} - <span class="icon-[material-symbols--hard-drive] mr-0.5"></span> - {/snippet} - </Pill> - </div> - </Card> + </ArchiveCard> {/snippet} </Selectable> {:else} diff --git a/frontend/src/routes/artists/+page.svelte b/frontend/src/routes/artists/+page.svelte index 9f0d893..9fff9d2 100644 --- a/frontend/src/routes/artists/+page.svelte +++ b/frontend/src/routes/artists/+page.svelte @@ -20,6 +20,7 @@ import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -69,6 +70,7 @@ {/snippet} {#snippet center()} <Search name="Artists" {filter} bind:field={filter.include.name.contains} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={ArtistSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/frontend/src/routes/characters/+page.svelte b/frontend/src/routes/characters/+page.svelte index 3a4b737..970a8fa 100644 --- a/frontend/src/routes/characters/+page.svelte +++ b/frontend/src/routes/characters/+page.svelte @@ -20,6 +20,7 @@ import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -69,6 +70,7 @@ {/snippet} {#snippet center()} <Search name="Characters" {filter} bind:field={filter.include.name.contains} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={CharacterSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/frontend/src/routes/circles/+page.svelte b/frontend/src/routes/circles/+page.svelte index 8bac7ed..bad1e1d 100644 --- a/frontend/src/routes/circles/+page.svelte +++ b/frontend/src/routes/circles/+page.svelte @@ -20,6 +20,7 @@ import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -69,6 +70,7 @@ {/snippet} {#snippet center()} <Search name="Circles" {filter} bind:field={filter.include.name.contains} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={CircleSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/frontend/src/routes/comics/+page.svelte b/frontend/src/routes/comics/+page.svelte index 03123be..11289e7 100644 --- a/frontend/src/routes/comics/+page.svelte +++ b/frontend/src/routes/comics/+page.svelte @@ -4,7 +4,7 @@ import { type ComicFragment } from '$gql/graphql'; import { ComicSortLabel } from '$lib/Enums'; import { ComicFilterContext } from '$lib/Filter.svelte'; - import Card, { comicCard } from '$lib/components/Card.svelte'; + import ComicCard from '$lib/components/ComicCard.svelte'; import Empty from '$lib/components/Empty.svelte'; import Guard from '$lib/components/Guard.svelte'; import Head from '$lib/components/Head.svelte'; @@ -13,7 +13,6 @@ import UpdateComicsDialog from '$lib/dialogs/UpdateComics.svelte'; import ComicFilterForm from '$lib/filter/ComicFilterForm.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; - import ComicPills from '$lib/pills/ComicPills.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; @@ -96,12 +95,11 @@ {#each comics.edges as comic, index (comic.id)} <Selectable {index} id={comic.id}> {#snippet children({ onclick, selected })} - <Card {...comicCard(comic)} {onclick}> + <ComicCard {comic} {onclick}> {#snippet overlay()} <SelectionOverlay position="left" {selected} /> {/snippet} - <ComicPills {comic} /> - </Card> + </ComicCard> {/snippet} </Selectable> {:else} diff --git a/frontend/src/routes/namespaces/+page.svelte b/frontend/src/routes/namespaces/+page.svelte index d8e728d..429432f 100644 --- a/frontend/src/routes/namespaces/+page.svelte +++ b/frontend/src/routes/namespaces/+page.svelte @@ -3,7 +3,7 @@ import { fetchNamespace, namespacesQuery } from '$gql/Queries'; import type { Namespace } from '$gql/graphql'; import { NamespaceSortLabel } from '$lib/Enums'; - import { BasicFilterContext } from '$lib/Filter.svelte'; + import { NamespaceFilterContext } from '$lib/Filter.svelte'; import { quickComicFilter } from '$lib/Navigation'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; @@ -20,6 +20,7 @@ import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -44,9 +45,9 @@ } }); - let filter = $state(new BasicFilterContext(data.filter)); + let filter = $state(new NamespaceFilterContext(data.filter)); $effect(() => { - filter = new BasicFilterContext(data.filter); + filter = new NamespaceFilterContext(data.filter); }); const edit = (id: number) => { @@ -69,6 +70,7 @@ {/snippet} {#snippet center()} <Search name="Namespaces" {filter} bind:field={filter.include.name.contains} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={NamespaceSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte index f71267f..2cb6f87 100644 --- a/frontend/src/routes/tags/+page.svelte +++ b/frontend/src/routes/tags/+page.svelte @@ -23,6 +23,7 @@ import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import EditSelection from '$lib/toolbar/EditSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -76,6 +77,7 @@ {#snippet center({ expanded, toggle })} <Search name="Tags" {filter} bind:field={filter.include.name.contains} /> <ToggleAdvancedFilters {expanded} {toggle} {filterSize} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={TagSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/frontend/src/routes/worlds/+page.svelte b/frontend/src/routes/worlds/+page.svelte index 6b95142..133dc27 100644 --- a/frontend/src/routes/worlds/+page.svelte +++ b/frontend/src/routes/worlds/+page.svelte @@ -20,6 +20,7 @@ import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte'; import Search from '$lib/toolbar/Search.svelte'; import SelectItems from '$lib/toolbar/SelectItems.svelte'; import SelectSort from '$lib/toolbar/SelectSort.svelte'; @@ -69,6 +70,7 @@ {/snippet} {#snippet center()} <Search name="Worlds" {filter} bind:field={filter.include.name.contains} /> + <FilterOrphaned {filter} /> <SelectSort {sort} labels={WorldSortLabel} /> <SelectItems {pagination} /> {/snippet} diff --git a/src/hircine/api/filters.py b/src/hircine/api/filters.py index 807178b..7ed5649 100644 --- a/src/hircine/api/filters.py +++ b/src/hircine/api/filters.py @@ -7,7 +7,7 @@ from strawberry import UNSET import hircine.db from hircine.db.models import ComicTag -from hircine.enums import Category, Censorship, Language, Rating +from hircine.enums import Category, Censorship, Language, Operator, Rating T = TypeVar("T") @@ -28,11 +28,23 @@ class Matchable(ABC): @strawberry.input +class CountFilter: + operator: Optional[Operator] = Operator.EQUAL + value: int + + def include(self, column, sql): + return sql.where(self.operator.value(column, self.value)) + + def exclude(self, column, sql): + return sql.where(~self.operator.value(column, self.value)) + + +@strawberry.input class AssociationFilter(Matchable): any: Optional[list[int]] = strawberry.field(default_factory=lambda: None) all: Optional[list[int]] = strawberry.field(default_factory=lambda: None) exact: Optional[list[int]] = strawberry.field(default_factory=lambda: None) - empty: Optional[bool] = None + count: Optional[CountFilter] = UNSET def _exists(self, condition): # The property.primaryjoin expression specifies the primary join path @@ -71,12 +83,6 @@ class AssociationFilter(Matchable): def _where_not_all_exist(self, sql): return sql.where(~self._all_exist(self.all)) - def _empty(self): - if self.empty: - return ~self._exists(True) - else: - return self._exists(True) - def _count_of(self, column): return ( select(func.count(column)) @@ -117,8 +123,8 @@ class AssociationFilter(Matchable): elif self.all == []: sql = sql.where(False) - if self.empty is not None: - sql = sql.where(self._empty()) + if self.count: + sql = self.count.include(self.count_column, sql) if self.exact is not None: sql = sql.where(self._exact()) @@ -134,8 +140,8 @@ class AssociationFilter(Matchable): if self.all: sql = self._where_not_all_exist(sql) - if self.empty is not None: - sql = sql.where(~self._empty()) + if self.count: + sql = self.count.exclude(self.count_column, sql) if self.exact is not None: sql = sql.where(~self._exact()) @@ -160,8 +166,14 @@ class Root: column = getattr(self._model, field, None) + # count columns are historically singular, so we need this hack + singular_field = field[:-1] + count_column = getattr(self._model, f"{singular_field}_count", None) + if issubclass(type(matcher), Matchable): matcher.column = column + matcher.count_column = count_column + if not negate: sql = matcher.include(sql) else: @@ -213,6 +225,17 @@ class StringFilter(Matchable): @strawberry.input +class BasicCountFilter(Matchable): + count: CountFilter + + def include(self, sql): + return self.count.include(self.count_column, sql) + + def exclude(self, sql): + return self.count.exclude(self.count_column, sql) + + +@strawberry.input class TagAssociationFilter(AssociationFilter): """ Tags need special handling since their IDs are strings instead of numbers. @@ -314,24 +337,28 @@ class ArchiveFilter(Root): @strawberry.input class ArtistFilter(Root): name: Optional[StringFilter] = UNSET + comics: Optional[BasicCountFilter] = UNSET @hircine.db.model("Character") @strawberry.input class CharacterFilter(Root): name: Optional[StringFilter] = UNSET + comics: Optional[BasicCountFilter] = UNSET @hircine.db.model("Circle") @strawberry.input class CircleFilter(Root): name: Optional[StringFilter] = UNSET + comics: Optional[BasicCountFilter] = UNSET @hircine.db.model("Namespace") @strawberry.input class NamespaceFilter(Root): name: Optional[StringFilter] = UNSET + tags: Optional[BasicCountFilter] = UNSET @hircine.db.model("Tag") @@ -339,9 +366,11 @@ class NamespaceFilter(Root): class TagFilter(Root): name: Optional[StringFilter] = UNSET namespaces: Optional[AssociationFilter] = UNSET + comics: Optional[BasicCountFilter] = UNSET @hircine.db.model("World") @strawberry.input class WorldFilter(Root): name: Optional[StringFilter] = UNSET + comics: Optional[BasicCountFilter] = UNSET diff --git a/src/hircine/api/sort.py b/src/hircine/api/sort.py index 17043a6..a4ccaf1 100644 --- a/src/hircine/api/sort.py +++ b/src/hircine/api/sort.py @@ -22,6 +22,10 @@ class ComicSort(enum.Enum): DATE = strawberry.enum_value(models.Comic.date) CREATED_AT = strawberry.enum_value(models.Comic.created_at) UPDATED_AT = strawberry.enum_value(models.Comic.updated_at) + ARTIST_COUNT = strawberry.enum_value(models.Comic.artist_count) + CHARACTER_COUNT = strawberry.enum_value(models.Comic.character_count) + CIRCLE_COUNT = strawberry.enum_value(models.Comic.circle_count) + WORLD_COUNT = strawberry.enum_value(models.Comic.world_count) TAG_COUNT = strawberry.enum_value(models.Comic.tag_count) PAGE_COUNT = strawberry.enum_value(models.Comic.page_count) RANDOM = "Random" @@ -41,6 +45,7 @@ class ArtistSort(enum.Enum): NAME = strawberry.enum_value(models.Artist.name) CREATED_AT = strawberry.enum_value(models.Artist.created_at) UPDATED_AT = strawberry.enum_value(models.Artist.updated_at) + COMIC_COUNT = strawberry.enum_value(models.Artist.comic_count) RANDOM = "Random" @@ -49,6 +54,7 @@ class CharacterSort(enum.Enum): NAME = strawberry.enum_value(models.Character.name) CREATED_AT = strawberry.enum_value(models.Character.created_at) UPDATED_AT = strawberry.enum_value(models.Character.updated_at) + COMIC_COUNT = strawberry.enum_value(models.Character.comic_count) RANDOM = "Random" @@ -57,6 +63,7 @@ class CircleSort(enum.Enum): NAME = strawberry.enum_value(models.Circle.name) CREATED_AT = strawberry.enum_value(models.Circle.created_at) UPDATED_AT = strawberry.enum_value(models.Circle.updated_at) + COMIC_COUNT = strawberry.enum_value(models.Circle.comic_count) RANDOM = "Random" @@ -66,6 +73,7 @@ class NamespaceSort(enum.Enum): NAME = strawberry.enum_value(models.Namespace.name) CREATED_AT = strawberry.enum_value(models.Namespace.created_at) UPDATED_AT = strawberry.enum_value(models.Namespace.updated_at) + TAG_COUNT = strawberry.enum_value(models.Namespace.tag_count) RANDOM = "Random" @@ -74,6 +82,8 @@ class TagSort(enum.Enum): NAME = strawberry.enum_value(models.Tag.name) CREATED_AT = strawberry.enum_value(models.Tag.created_at) UPDATED_AT = strawberry.enum_value(models.Tag.updated_at) + COMIC_COUNT = strawberry.enum_value(models.Tag.comic_count) + NAMESPACE_COUNT = strawberry.enum_value(models.Tag.namespace_count) RANDOM = "Random" @@ -82,6 +92,7 @@ class WorldSort(enum.Enum): NAME = strawberry.enum_value(models.World.name) CREATED_AT = strawberry.enum_value(models.World.created_at) UPDATED_AT = strawberry.enum_value(models.World.updated_at) + COMIC_COUNT = strawberry.enum_value(models.World.comic_count) RANDOM = "Random" diff --git a/src/hircine/db/models.py b/src/hircine/db/models.py index f204998..5d1a59a 100644 --- a/src/hircine/db/models.py +++ b/src/hircine/db/models.py @@ -356,7 +356,10 @@ class ComicWorld(Base): def defer_relationship_count(relationship, secondary=False): - left, right = relationship.property.synchronize_pairs[0] + if secondary: + left, right = relationship.property.secondary_synchronize_pairs[0] + else: + left, right = relationship.property.synchronize_pairs[0] return deferred( select(func.count(right)) @@ -366,7 +369,23 @@ def defer_relationship_count(relationship, secondary=False): ) +Comic.artist_count = defer_relationship_count(Comic.artists) +Comic.character_count = defer_relationship_count(Comic.characters) +Comic.circle_count = defer_relationship_count(Comic.circles) Comic.tag_count = defer_relationship_count(Comic.tags) +Comic.world_count = defer_relationship_count(Comic.worlds) + +Artist.comic_count = defer_relationship_count(Comic.artists, secondary=True) +Character.comic_count = defer_relationship_count(Comic.characters, secondary=True) +Circle.comic_count = defer_relationship_count(Comic.circles, secondary=True) +Namespace.tag_count = defer_relationship_count(Tag.namespaces, secondary=True) +Tag.comic_count = deferred( + select(func.count(ComicTag.tag_id)) + .where(Tag.id == ComicTag.tag_id) + .scalar_subquery() +) +Tag.namespace_count = defer_relationship_count(Tag.namespaces) +World.comic_count = defer_relationship_count(Comic.worlds, secondary=True) @event.listens_for(Comic.pages, "bulk_replace") diff --git a/src/hircine/enums.py b/src/hircine/enums.py index 7f95f02..f267270 100644 --- a/src/hircine/enums.py +++ b/src/hircine/enums.py @@ -1,4 +1,5 @@ import enum +import operator import strawberry @@ -57,6 +58,13 @@ class OnMissing(enum.Enum): @strawberry.enum +class Operator(enum.Enum): + GREATER_THAN = operator.gt + LOWER_THAN = operator.lt + EQUAL = operator.eq + + +@strawberry.enum class Language(enum.Enum): AA = "Afar" AB = "Abkhazian" diff --git a/tests/api/test_filter.py b/tests/api/test_filter.py index 1438785..6eb2934 100644 --- a/tests/api/test_filter.py +++ b/tests/api/test_filter.py @@ -421,51 +421,59 @@ async def test_field_presence(query_comic_filter, gen_comic, empty_comic, filter "filter,ids", [ ( - {"include": {"artists": {"empty": True}}}, + {"include": {"artists": {"count": {"value": 0}}}}, [100], ), ( - {"include": {"artists": {"empty": False}}}, - [1, 2], + {"include": {"artists": {"count": {"value": 0, "operator": "EQUAL"}}}}, + [100], ), ( - {"exclude": {"artists": {"empty": True}}}, - [1, 2], + { + "include": { + "artists": {"count": {"value": 1, "operator": "GREATER_THAN"}} + } + }, + [1], ), ( - {"exclude": {"artists": {"empty": False}}}, - [100], + {"include": {"artists": {"count": {"value": 3, "operator": "LOWER_THAN"}}}}, + [1, 2, 100], ), ( - {"include": {"tags": {"empty": True}}}, - [100], + {"exclude": {"artists": {"count": {"value": 0}}}}, + [1, 2], ), ( - {"include": {"tags": {"empty": False}}}, + {"exclude": {"artists": {"count": {"value": 0, "operator": "EQUAL"}}}}, [1, 2], ), ( - {"exclude": {"tags": {"empty": True}}}, - [1, 2], + { + "exclude": { + "artists": {"count": {"value": 1, "operator": "GREATER_THAN"}} + } + }, + [2, 100], ), ( - {"exclude": {"tags": {"empty": False}}}, - [100], + {"exclude": {"artists": {"count": {"value": 3, "operator": "LOWER_THAN"}}}}, + [], ), ], ids=[ - "includes artist empty", - "includes artist not empty", - "excludes artist empty", - "excludes artist not empty", - "includes tags empty", - "includes tags not empty", - "excludes tags empty", - "excludes tags not empty", + "include equal (default)", + "include equal (explicit)", + "include greater than", + "include lower than", + "exclude equal (default)", + "exclude equal (explicit)", + "exclude greater than", + "exclude lower than", ], ) @pytest.mark.anyio -async def test_assoc_presence(query_comic_filter, gen_comic, empty_comic, filter, ids): +async def test_assoc_counts(query_comic_filter, gen_comic, empty_comic, filter, ids): await DB.add(next(gen_comic)) await DB.add(next(gen_comic)) await DB.add(empty_comic) @@ -520,3 +528,48 @@ async def test_tag_assoc_filter(query_tag_filter, gen_namespace, gen_tag, filter response.assert_is("TagFilterResult") assert id_list(response.edges) == ids + + +@pytest.mark.parametrize( + "filter,expect", + [ + ({"include": {"comics": {"count": {"value": 1}}}}, [2, 3]), + ({"include": {"comics": {"count": {"value": 2, "operator": "EQUAL"}}}}, [1, 4]), + ( + { + "include": { + "comics": {"count": {"value": 3, "operator": "GREATER_THAN"}} + } + }, + [], + ), + ( + {"include": {"comics": {"count": {"value": 2, "operator": "LOWER_THAN"}}}}, + [2, 3], + ), + ( + {"exclude": {"comics": {"count": {"value": 1}}}}, + [1, 4], + ), + ( + {"exclude": {"comics": {"count": {"value": 1, "operator": "LOWER_THAN"}}}}, + [1, 2, 3, 4], + ), + ], + ids=[ + "include equal (default)", + "include equal (explicit)", + "include greater than", + "include lower than", + "exclude equal (default)", + "exclude lower than", + ], +) +@pytest.mark.anyio +async def test_count_filter(query_string_filter, gen_comic, filter, expect): + await DB.add_all(*gen_comic) + + response = Response(await query_string_filter(filter)) + response.assert_is("ArtistFilterResult") + + assert id_list(response.edges) == expect diff --git a/tests/api/test_sort.py b/tests/api/test_sort.py index 404e3d6..65a4990 100644 --- a/tests/api/test_sort.py +++ b/tests/api/test_sort.py @@ -1,7 +1,7 @@ import pytest from conftest import DB, Response -from hircine.db.models import Namespace +from hircine.db.models import Namespace, Tag @pytest.fixture @@ -23,6 +23,24 @@ def query_comic_sort(execute_sort): @pytest.fixture +def query_artist_sort(execute_sort): + query = """ + query artists($sort: ArtistSortInput) { + artists(sort: $sort) { + __typename + count + edges { + id + name + } + } + } + """ + + return execute_sort(query) + + +@pytest.fixture def query_namespace_sort(execute_sort): query = """ query namespaces($sort: NamespaceSortInput) { @@ -88,6 +106,31 @@ async def test_query_comics_sort_tag_count(gen_comic, query_comic_sort, sort, re assert ids == [edge["id"] for edge in response.edges] +@pytest.mark.parametrize( + "sort,reverse,expect", + [ + ({"on": "COMIC_COUNT"}, False, [2, 3, 1, 4]), + ({"on": "COMIC_COUNT", "direction": "DESCENDING"}, True, [1, 4, 2, 3]), + ({"on": "COMIC_COUNT", "direction": "ASCENDING"}, False, [2, 3, 1, 4]), + ], + ids=[ + "ascending (default)", + "descending", + "ascending", + ], +) +@pytest.mark.anyio +async def test_query_artists_sort_comic_count( + gen_comic, query_artist_sort, sort, reverse, expect +): + await DB.add_all(*gen_comic) + + response = Response(await query_artist_sort(sort)) + response.assert_is("ArtistFilterResult") + + assert expect == [edge["id"] for edge in response.edges] + + @pytest.mark.anyio async def test_query_comics_sort_random(gen_comic, query_comic_sort): comics = await DB.add_all(*gen_comic) @@ -136,3 +179,33 @@ async def test_query_namespace_sort_sort_name(query_namespace_sort): response.assert_is("NamespaceFilterResult") assert [edge["name"] for edge in response.edges] == ["two", "one"] + +@pytest.mark.parametrize( + "sort,reverse,expect", + [ + ({"on": "TAG_COUNT"}, False, [2, 1]), + ({"on": "TAG_COUNT", "direction": "DESCENDING"}, True, [1, 2]), + ({"on": "TAG_COUNT", "direction": "ASCENDING"}, False, [2, 1]), + ], + ids=[ + "ascending (default)", + "descending", + "ascending", + ], +) +@pytest.mark.anyio +async def test_query_namespace_sort_tag_count( + gen_comic, query_namespace_sort, sort, reverse, expect +): + namespace_foo = Namespace(id=1, name="foo") + namespace_bar = Namespace(id=2, name="bar") + + tag_foo = Tag(id=1, name="foo", namespaces=[namespace_foo]) + tag_bar = Tag(id=2, name="bar", namespaces=[namespace_foo, namespace_bar]) + + await DB.add_all(tag_foo, tag_bar) + + response = Response(await query_namespace_sort(sort)) + response.assert_is("NamespaceFilterResult") + + assert expect == [edge["id"] for edge in response.edges] |