Skip to content

Commit 6ed0f9a

Browse files
authored
Display if media in watchlist/seen (#809)
* feat(backend): store whether it is in watchlist * feat(database): migrate user_to_entity fields * feat(backend): hoist join out of conditional query * feat(backend): return media reason for metadata list * refactor(frontend): extract var * feat(frontend): pass `reason` to media component * refactor(backend): rewrite cleanup function * fix(backend): remove extremely verbose log * fix(backend): delete old user_to_entity * refactor(backend): change name of var * refactor(frontend): change order of funcs * fix(frontend): remove useless prop * refactor(frontend): extract component * feat(frontend): display icon for media reason * build(backend): bump version * refactor(frontend): extract common prop into style * refactor(*): change order of imports
1 parent 9bc97e4 commit 6ed0f9a

File tree

14 files changed

+201
-242
lines changed

14 files changed

+201
-242
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ryot"
3-
version = "5.1.0"
3+
version = "5.1.1"
44
edition = "2021"
55
repository = "https://github.com/IgnisDa/ryot"
66
license = "GPL-3.0"

apps/backend/src/miscellaneous/resolver.rs

+85-197
Large diffs are not rendered by default.

apps/backend/src/models.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ pub mod media {
261261
pub struct MediaListItem {
262262
pub data: MetadataSearchItem,
263263
pub average_rating: Option<Decimal>,
264+
pub media_reason: Vec<UserToMediaReason>,
264265
}
265266

266267
#[derive(Debug, Serialize, Deserialize, SimpleObject, Clone, FromQueryResult)]
@@ -892,7 +893,17 @@ pub mod media {
892893
}
893894

894895
#[derive(
895-
Copy, Clone, Debug, PartialEq, Eq, DeriveActiveEnum, EnumIter, Serialize, Deserialize, Hash,
896+
Copy,
897+
Clone,
898+
Debug,
899+
Enum,
900+
PartialEq,
901+
Eq,
902+
DeriveActiveEnum,
903+
EnumIter,
904+
Serialize,
905+
Deserialize,
906+
Hash,
896907
)]
897908
#[sea_orm(rs_type = "String", db_type = "String(None)")]
898909
pub enum UserToMediaReason {
@@ -908,6 +919,8 @@ pub mod media {
908919
Owned,
909920
#[sea_orm(string_value = "Monitoring")]
910921
Monitoring,
922+
#[sea_orm(string_value = "Watchlist")]
923+
Watchlist,
911924
}
912925

913926
#[derive(Debug, SimpleObject)]

apps/frontend/app/components/media.tsx

+54-28
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Image,
1616
Input,
1717
Loader,
18+
type MantineStyleProp,
1819
Menu,
1920
Modal,
2021
NumberInput,
@@ -28,6 +29,7 @@ import {
2829
Text,
2930
TextInput,
3031
Textarea,
32+
ThemeIcon,
3133
Title,
3234
Tooltip,
3335
useComputedColorScheme,
@@ -50,6 +52,7 @@ import {
5052
type ReviewItem,
5153
type UserMediaReminderPartFragment,
5254
UserReviewScale,
55+
UserToMediaReason,
5356
Visibility,
5457
} from "@ryot/generated/graphql/backend/graphql";
5558
import { changeCase, formatDateToNaiveDate, getInitials } from "@ryot/ts-utils";
@@ -58,10 +61,12 @@ import {
5861
IconArrowBigUp,
5962
IconArrowsRight,
6063
IconBackpack,
64+
IconBookmarksFilled,
6165
IconCheck,
6266
IconCloudDownload,
6367
IconEdit,
6468
IconPercentage,
69+
IconRosetteDiscountCheck,
6570
IconStarFilled,
6671
IconTrash,
6772
IconX,
@@ -444,6 +449,12 @@ export const ReviewItemDisplay = (props: {
444449
);
445450
};
446451

452+
const blackBgStyles = {
453+
backgroundColor: "rgba(0, 0, 0, 0.75)",
454+
borderRadius: 3,
455+
padding: 2,
456+
} satisfies MantineStyleProp;
457+
447458
export const BaseDisplayItem = (props: {
448459
name: string;
449460
onClick?: (e: React.MouseEvent) => Promise<void>;
@@ -457,6 +468,7 @@ export const BaseDisplayItem = (props: {
457468
highlightRightText?: string;
458469
children?: ReactNode;
459470
nameRight?: JSX.Element;
471+
mediaReason?: UserToMediaReason[];
460472
}) => {
461473
const colorScheme = useComputedColorScheme("dark");
462474

@@ -480,13 +492,18 @@ export const BaseDisplayItem = (props: {
480492
</Box>
481493
);
482494

495+
const themeIconSurrounder = (idx: number, icon?: JSX.Element) => (
496+
<ThemeIcon variant="transparent" size="sm" color="lime" key={idx}>
497+
{icon}
498+
</ThemeIcon>
499+
);
500+
483501
return (
484502
<Flex
485503
key={`${props.bottomLeft}-${props.bottomRight}-${props.name}`}
486504
align="center"
487505
justify="center"
488506
direction="column"
489-
pos="relative"
490507
>
491508
{props.topLeft}
492509
<SurroundingElement style={{ flex: "none" }} pos="relative">
@@ -510,7 +527,32 @@ export const BaseDisplayItem = (props: {
510527
getInitials(props.name),
511528
)}
512529
/>
513-
{props.topRight}
530+
<Box pos="absolute" top={5} right={5}>
531+
{props.topRight}
532+
</Box>
533+
{props.mediaReason ? (
534+
<Group
535+
style={blackBgStyles}
536+
pos="absolute"
537+
bottom={5}
538+
left={5}
539+
gap="xs"
540+
>
541+
{props.mediaReason
542+
.map((r) =>
543+
match(r)
544+
.with(UserToMediaReason.Seen, () => (
545+
<IconRosetteDiscountCheck />
546+
))
547+
.with(UserToMediaReason.Watchlist, () => (
548+
<IconBookmarksFilled />
549+
))
550+
.otherwise(() => undefined),
551+
)
552+
.filter(Boolean)
553+
.map((icon, idx) => themeIconSurrounder(idx, icon))}
554+
</Group>
555+
) : null}
514556
</SurroundingElement>
515557
<Flex w="100%" direction="column" px={{ base: 10, md: 3 }} py={4}>
516558
<Flex justify="space-between" direction="row" w="100%">
@@ -563,8 +605,10 @@ export const MediaItemWithoutUpdateModal = (props: {
563605
noHref?: boolean;
564606
onClick?: (e: React.MouseEvent) => Promise<void>;
565607
nameRight?: JSX.Element;
608+
mediaReason?: UserToMediaReason[];
566609
}) => {
567610
const navigate = useNavigate();
611+
const id = props.item.identifier;
568612

569613
return (
570614
<BaseDisplayItem
@@ -575,25 +619,19 @@ export const MediaItemWithoutUpdateModal = (props: {
575619
? props.href
576620
: match(props.entityLot)
577621
.with(EntityLot.Media, undefined, null, () =>
578-
$path("/media/item/:id", { id: props.item.identifier }),
622+
$path("/media/item/:id", { id }),
579623
)
580624
.with(EntityLot.MediaGroup, () =>
581-
$path("/media/groups/item/:id", {
582-
id: props.item.identifier,
583-
}),
625+
$path("/media/groups/item/:id", { id }),
584626
)
585627
.with(EntityLot.Person, () =>
586-
$path("/media/people/item/:id", {
587-
id: props.item.identifier,
588-
}),
628+
$path("/media/people/item/:id", { id }),
589629
)
590630
.with(EntityLot.Exercise, () =>
591-
$path("/fitness/exercises/item/:id", {
592-
id: props.item.identifier,
593-
}),
631+
$path("/fitness/exercises/item/:id", { id }),
594632
)
595633
.with(EntityLot.Collection, () =>
596-
$path("/collections/:id", { id: props.item.identifier }),
634+
$path("/collections/:id", { id }),
597635
)
598636
.exhaustive()
599637
: undefined
@@ -613,18 +651,10 @@ export const MediaItemWithoutUpdateModal = (props: {
613651
/>
614652
) : null
615653
}
654+
mediaReason={props.mediaReason}
616655
topRight={
617656
props.averageRating ? (
618-
<Box
619-
p={2}
620-
pos="absolute"
621-
top={5}
622-
right={5}
623-
style={{
624-
backgroundColor: "rgba(0, 0, 0, 0.75)",
625-
borderRadius: 3,
626-
}}
627-
>
657+
<Box style={blackBgStyles}>
628658
<Flex align="center" gap={4}>
629659
<IconStarFilled size={12} style={{ color: "#EBE600FF" }} />
630660
<Text c="white" size="xs" fw="bold" pr={4}>
@@ -656,11 +686,7 @@ export const MediaItemWithoutUpdateModal = (props: {
656686
onClick={(e) => {
657687
e.preventDefault();
658688
navigate(
659-
$path(
660-
"/media/item/:id",
661-
{ id: props.item.identifier },
662-
{ openReviewModal: true },
663-
),
689+
$path("/media/item/:id", { id }, { openReviewModal: true }),
664690
);
665691
}}
666692
>

apps/frontend/app/lib/hooks.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ export function useGetMantineColor() {
2020
export function useSearchParam() {
2121
const [searchParams, setSearchParams] = useSearchParams();
2222

23-
const setP = (key: string, value?: string | null) => {
23+
const delP = (key: string) => {
2424
setSearchParams((prev) => {
25-
if (!value) delP(key);
26-
else prev.set(key, value);
25+
prev.delete(key);
2726
return prev;
2827
});
2928
};
3029

31-
const delP = (key: string) => {
30+
const setP = (key: string, value?: string | null) => {
3231
setSearchParams((prev) => {
33-
prev.delete(key);
32+
if (!value) delP(key);
33+
else prev.set(key, value);
3434
return prev;
3535
});
3636
};

apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export default function Page() {
397397
publishYear: lm.data.publishYear?.toString(),
398398
}}
399399
averageRating={lm.averageRating ?? undefined}
400+
mediaReason={lm.mediaReason}
400401
lot={loaderData.lot}
401402
href={$path("/media/item/:id", {
402403
id: lm.data.identifier,

apps/landing/astro.config.mjs

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import sitemap from "@astrojs/sitemap";
22
import tailwind from "@astrojs/tailwind";
3-
import { defineConfig } from "astro/config";
43
import robotsTxt from "astro-robots-txt";
4+
import { defineConfig } from "astro/config";
55

66
// https://astro.build/config
77
export default defineConfig({
8-
site: import.meta.env.DEV
9-
? "http://localhost:4200"
10-
: "https://ryot.io/",
8+
site: import.meta.env.DEV ? "http://localhost:4200" : "https://ryot.io/",
119
integrations: [tailwind(), sitemap(), robotsTxt()],
1210
});

apps/landing/src/layouts/Layout.astro

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Meta } from "@/config/landing.interface";
44
import "@fontsource-variable/rubik";
55
66
interface Props {
7-
meta: Meta;
7+
meta: Meta;
88
}
99
1010
const { meta } = Astro.props;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
let db = manager.get_connection();
10+
db.execute_unprepared(r#"update user_to_entity set needs_to_be_updated = true;"#)
11+
.await?;
12+
13+
Ok(())
14+
}
15+
16+
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
17+
Ok(())
18+
}
19+
}

libs/database/src/migrations/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod m20231219_create_metadata_relations;
2020
mod m20240415_is_v5_migration;
2121
mod m20240416_change_json_to_generic_json;
2222
mod m20240425_add_created_by_user_id_column_to_execise;
23+
mod m20240503_update_user_to_entity_to_recalculate;
2324

2425
pub use m20230410_create_metadata::Metadata as AliasedMetadata;
2526
pub use m20230413_create_person::Person as AliasedPerson;
@@ -57,6 +58,7 @@ impl MigratorTrait for Migrator {
5758
Box::new(m20240415_is_v5_migration::Migration),
5859
Box::new(m20240416_change_json_to_generic_json::Migration),
5960
Box::new(m20240425_add_created_by_user_id_column_to_execise::Migration),
61+
Box::new(m20240503_update_user_to_entity_to_recalculate::Migration),
6062
]
6163
}
6264
}

libs/generated/src/graphql/backend/gql.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const documents = {
2929
"query MetadataGroupDetails($metadataGroupId: Int!) {\n metadataGroupDetails(metadataGroupId: $metadataGroupId) {\n details {\n id\n title\n lot\n source\n displayImages\n parts\n isPartial\n }\n sourceUrl\n contents {\n ...PartialMetadataPart\n }\n }\n}": types.MetadataGroupDetailsDocument,
3030
"query MetadataGroupSearch($input: MetadataGroupSearchInput!) {\n metadataGroupSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n identifier\n name\n image\n parts\n }\n }\n}": types.MetadataGroupSearchDocument,
3131
"query MetadataGroupsList($input: SearchInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n title\n lot\n parts\n image\n }\n }\n}": types.MetadataGroupsListDocument,
32-
"query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}": types.MetadataListDocument,
32+
"query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n mediaReason\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}": types.MetadataListDocument,
3333
"query MetadataMainDetails($metadataId: Int!) {\n metadataDetails(metadataId: $metadataId) {\n title\n lot\n source\n isNsfw\n isPartial\n sourceUrl\n identifier\n description\n publishYear\n publishDate\n providerRating\n productionStatus\n originalLanguage\n genres {\n id\n name\n }\n group {\n id\n name\n part\n }\n assets {\n images\n videos {\n videoId\n source\n }\n }\n }\n}": types.MetadataMainDetailsDocument,
3434
"query MetadataSearch($input: MetadataSearchInput!) {\n metadataSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n databaseId\n hasInteracted\n item {\n identifier\n title\n image\n publishYear\n }\n }\n }\n}": types.MetadataSearchDocument,
3535
"query PeopleSearch($input: PeopleSearchInput!) {\n peopleSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n identifier\n name\n image\n birthYear\n }\n }\n}": types.PeopleSearchDocument,
@@ -130,7 +130,7 @@ export function graphql(source: "query MetadataGroupsList($input: SearchInput!)
130130
/**
131131
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
132132
*/
133-
export function graphql(source: "query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}"): (typeof documents)["query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}"];
133+
export function graphql(source: "query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n mediaReason\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}"): (typeof documents)["query MetadataList($input: MetadataListInput!) {\n metadataList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n averageRating\n mediaReason\n data {\n ...MetadataSearchItemPart\n }\n }\n }\n}"];
134134
/**
135135
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
136136
*/

0 commit comments

Comments
 (0)