diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index ea4a7d33bf..77d41bbeb5 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -66,6 +66,9 @@ export class UserSettings { @Column({ nullable: true }) public watchlistSyncTv?: boolean; + @Column({ nullable: true }) + public collapseTags?: boolean; + @Column({ type: 'text', nullable: true, diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index fb0767b211..050ef825b3 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -16,6 +16,7 @@ export interface UserSettingsGeneralResponse { globalTvQuotaDays?: number; watchlistSyncMovies?: boolean; watchlistSyncTv?: boolean; + collapseTags?: boolean; } export type NotificationAgentTypes = Record; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 10213a0403..799bff9ad2 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -97,6 +97,7 @@ export interface MainSettings { tv: Quota; }; hideAvailable: boolean; + collapseTags: boolean; localLogin: boolean; newPlexLogin: boolean; region: string; @@ -293,6 +294,7 @@ class Settings { tv: {}, }, hideAvailable: false, + collapseTags: false, localLogin: true, newPlexLogin: true, region: '', diff --git a/server/migration/1710339059307-AddCollapseTagsColumn.ts b/server/migration/1710339059307-AddCollapseTagsColumn.ts new file mode 100644 index 0000000000..fa8b6d66cc --- /dev/null +++ b/server/migration/1710339059307-AddCollapseTagsColumn.ts @@ -0,0 +1,31 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCollapseTagsColumn1710339059307 implements MigrationInterface { + name = 'AddCollapseTagsColumn1710339059307'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "region" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "collapseTags" boolean, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_settings"("id", "locale", "region", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "region", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "region" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_settings"("id", "locale", "region", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "region", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index c8b3f50bd2..e72e104466 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -65,6 +65,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( globalTvQuotaLimit: defaultQuotas.tv.quotaLimit, watchlistSyncMovies: user.settings?.watchlistSyncMovies, watchlistSyncTv: user.settings?.watchlistSyncTv, + collapseTags: user.settings?.collapseTags, }); } catch (e) { next({ status: 500, message: e.message }); @@ -118,6 +119,7 @@ userSettingsRoutes.post< originalLanguage: req.body.originalLanguage, watchlistSyncMovies: req.body.watchlistSyncMovies, watchlistSyncTv: req.body.watchlistSyncTv, + collapseTags: req.body.collapseTags, }); } else { user.settings.discordId = req.body.discordId; @@ -126,6 +128,7 @@ userSettingsRoutes.post< user.settings.originalLanguage = req.body.originalLanguage; user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies; user.settings.watchlistSyncTv = req.body.watchlistSyncTv; + user.settings.collapseTags = req.body.collapseTags; } await userRepository.save(user); @@ -138,6 +141,7 @@ userSettingsRoutes.post< originalLanguage: user.settings.originalLanguage, watchlistSyncMovies: user.settings.watchlistSyncMovies, watchlistSyncTv: user.settings.watchlistSyncTv, + collapseTags: user.settings.collapseTags, }); } catch (e) { next({ status: 500, message: e.message }); diff --git a/src/components/KeywordDisclosure/index.tsx b/src/components/KeywordDisclosure/index.tsx new file mode 100644 index 0000000000..477fe7ed25 --- /dev/null +++ b/src/components/KeywordDisclosure/index.tsx @@ -0,0 +1,66 @@ +import Tag from '@app/components/Common/Tag'; +import { useUser } from '@app/hooks/useUser'; +import { Disclosure, Transition } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/solid'; +import type { Keyword } from '@server/models/common'; +import Link from 'next/link'; + +interface KeywordDisclosureProps { + keywords: Keyword[]; + type: 'tv' | 'movies'; +} + +const KeywordDisclosure = ({ keywords, type }: KeywordDisclosureProps) => { + const { user } = useUser(); + + return ( + <> + {keywords.length > 0 && ( + + {({ open }) => ( + <> + + Tags + + + + + {keywords.map((keyword) => ( + + + {keyword.name} + + + ))} + + + + )} + + )} + + ); +}; + +export default KeywordDisclosure; diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 11b2a77539..85a74ad274 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -10,10 +10,10 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import type { PlayButtonLink } from '@app/components/Common/PlayButton'; import PlayButton from '@app/components/Common/PlayButton'; -import Tag from '@app/components/Common/Tag'; import Tooltip from '@app/components/Common/Tooltip'; import ExternalLinkBlock from '@app/components/ExternalLinkBlock'; import IssueModal from '@app/components/IssueModal'; +import KeywordDisclosure from '@app/components/KeywordDisclosure'; import ManageSlideOver from '@app/components/ManageSlideOver'; import MediaSlider from '@app/components/MediaSlider'; import PersonCard from '@app/components/PersonCard'; @@ -464,20 +464,10 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { )} - {data.keywords.length > 0 && ( -
- {data.keywords.map((keyword) => ( - - - {keyword.name} - - - ))} -
- )} +
{data.collection && ( diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 2f44a7d8c1..688e0f491e 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -11,10 +11,10 @@ import PageTitle from '@app/components/Common/PageTitle'; import type { PlayButtonLink } from '@app/components/Common/PlayButton'; import PlayButton from '@app/components/Common/PlayButton'; import StatusBadgeMini from '@app/components/Common/StatusBadgeMini'; -import Tag from '@app/components/Common/Tag'; import Tooltip from '@app/components/Common/Tooltip'; import ExternalLinkBlock from '@app/components/ExternalLinkBlock'; import IssueModal from '@app/components/IssueModal'; +import KeywordDisclosure from '@app/components/KeywordDisclosure'; import ManageSlideOver from '@app/components/ManageSlideOver'; import MediaSlider from '@app/components/MediaSlider'; import PersonCard from '@app/components/PersonCard'; @@ -503,20 +503,10 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
)} - {data.keywords.length > 0 && ( -
- {data.keywords.map((keyword) => ( - - - {keyword.name} - - - ))} -
- )} +

{intl.formatMessage(messages.seasonstitle)}

{data.seasons diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 842ea7af26..1ff76cd1ed 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -55,6 +55,8 @@ const messages = defineMessages({ plexwatchlistsyncseries: 'Auto-Request Series', plexwatchlistsyncseriestip: 'Automatically request series on your Plex Watchlist', + collapsetags: 'Collapse Tags', + collapsetagstip: 'Collapse tags by default in Movie/Series detail page', }); const UserGeneralSettings = () => { @@ -130,6 +132,7 @@ const UserGeneralSettings = () => { tvQuotaDays: data?.tvQuotaDays, watchlistSyncMovies: data?.watchlistSyncMovies, watchlistSyncTv: data?.watchlistSyncTv, + collapseTags: data?.collapseTags, }} validationSchema={UserGeneralSettingsSchema} enableReinitialize @@ -149,6 +152,7 @@ const UserGeneralSettings = () => { tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null, watchlistSyncMovies: values.watchlistSyncMovies, watchlistSyncTv: values.watchlistSyncTv, + collapseTags: values.collapseTags, }); if (currentUser?.id === user?.id && setLocale) { @@ -334,6 +338,24 @@ const UserGeneralSettings = () => {
+
+ +
+ { + setFieldValue('collapseTags', !values.collapseTags); + }} + /> +
+
{currentHasPermission(Permission.MANAGE_USERS) && !hasPermission(Permission.MANAGE_USERS) && ( <> diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 192b3fe9da..1bef384671 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -33,6 +33,7 @@ export interface UserSettings { notificationTypes: Partial; watchlistSyncMovies?: boolean; watchlistSyncTv?: boolean; + collapseTags?: boolean; } interface UserHookResponse { diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 10165c9e15..9909c62562 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1087,6 +1087,8 @@ "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Override Global Limit", "components.UserProfile.UserSettings.UserGeneralSettings.general": "General", "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "General Settings", + "components.UserProfile.UserSettings.UserGeneralSettings.collapsetags": "Collapse Tags", + "components.UserProfile.UserSettings.UserGeneralSettings.collapsetagstip": "Collapse tags by default in Movie/Series detail page", "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Default ({language})", "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Local User", "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Movie Request Limit",