diff --git a/lang/de.slf b/lang/de.slf index 50450363f3ee221fe0af322f76cc2fe90763acc8..bd93f8448e10275677fd91419cb9b09f31dc7d85 100644 --- a/lang/de.slf +++ b/lang/de.slf @@ -155,6 +155,7 @@ ui.generic.close = "Schließen" ui.generic.lecture_given_by = "Gehalten von" // ui.generic.livestream_planned = #object.lecture.livestream_planned // ui.generic.livestream_live = "Live" +// ui.generic.object_invisible = "This object is invisible to regular users" // mir ist keine schöne Übersetzung eingefallen ui.generic.filter.any = "Alle" diff --git a/lang/en.slf b/lang/en.slf index 963e8d34977761fcce86e28319d98a71c0b9dc5c..553f70ecdf649649302dbe85c182ba7f1d570b37 100644 --- a/lang/en.slf +++ b/lang/en.slf @@ -159,6 +159,7 @@ ui.generic.close = "Close" ui.generic.lecture_given_by = "Given by" ui.generic.livestream_planned = #object.lecture.livestream_planned ui.generic.livestream_live = "Live" +ui.generic.object_invisible = "This object is invisible to regular users" ui.generic.filter.any = "Any" diff --git a/src/components/CourseListing.tsx b/src/components/CourseListing.tsx index be089d9569df7cbb120c94e52d7da08612a9aded..da3fef93c3ffb082f988db7ce490feed32173f24 100644 --- a/src/components/CourseListing.tsx +++ b/src/components/CourseListing.tsx @@ -1,4 +1,3 @@ -import { useBackendContext } from "./BackendProvider"; import Link from "next/link"; import { OMCreate, @@ -16,7 +15,6 @@ import type { GetCourseResponse, lecture } from "@/api/api_v1_types"; import { ResourceType } from "@/misc/PromiseHelpers"; import { AuthenticationMethodIcons } from "@/misc/Util"; import { useLanguage } from "./LanguageProvider"; -import { LectureLiveLabel } from "./LiveLabel"; function ListingHeader({ course }: { course: GetCourseResponse }) { const { editMode } = useEditMode(); @@ -24,9 +22,9 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { return ( <div className="card mb-3"> - <div className="card-body"> + <div className={`card-body ${course.is_visible === false ? "bg-danger-subtle" : ""}`}> <h5 className="card-title d-flex"> - <span className="panel-title flex-fill align-self-center"> + <span className="panel-title flex-fill"> <EmbeddedOMFieldComponent object_type="course" object_id={course.id!} @@ -35,9 +33,20 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { initialValue={course.full_name} /> </span> - <div style={{ fontSize: "1rem" /* Reset font size for icons */ }}> + <div + style={{ fontSize: "1rem" /* Reset font size for icons */ }} + className="d-flex align-items-center" + > {editMode && ( <> + <EmbeddedOMFieldComponent + object_type="course" + object_id={course.id!} + field_id="is_visible" + field_type="boolean" + initialValue={course.is_visible} + changeIndicator="background" + /> <OMHistory object_type="course" object_id={course.id!} /> <OMEdit object_type="course" object_id={course.id!} /> <OMDelete object_type="course" object_id={course.id!} /> @@ -111,6 +120,14 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { ) && <DownloadAllModal course={course} />} </div> </div> + {course.is_visible === false && ( + <> + <hr /> + <small className="text-muted"> + {language.get("ui.generic.object_invisible")} + </small> + </> + )} </div> </div> ); @@ -125,31 +142,38 @@ export function LectureListItem({ course_id_string?: string; [key: string]: any; }) { - const api = useBackendContext(); const { editMode } = useEditMode(); const { language } = useLanguage(); - let content = <></>; - - if (lecture.media_sources && lecture.media_sources.length > 0) { - let thumbUrl = lecture.thumbnail_url ?? `${api.assetUrl()}/thumbnail/l_${lecture.id}.jpg`; - const showDownloadSourcesButton = - editMode === false && - lecture.media_sources && - lecture.media_sources.some((ele) => ele.download_url !== undefined); + const showDownloadSourcesButton = + editMode === false && + lecture.media_sources && + lecture.media_sources.some((ele) => ele.download_url !== undefined); - content = ( - <> - <div - style={{ - backgroundImage: `url('${thumbUrl}')`, - }} - className="col-sm-2 col-12 thumbnailimg" - > - <Link href={`/${course_id_string ?? lecture.course_id}/${lecture.id}`}> - <span aria-hidden="true" className={"playpreviewbtn bi bi-play-circle"} /> - </Link> - </div> + return ( + <li + className={`list-group-item highlighting-list-item ${lecture.no_recording ? "text-muted" : ""} ${lecture.is_visible === false ? "bg-danger-subtle" : ""}`} + id={`lecture-${lecture.id}`} + {...props} + > + <div className="row"> + {lecture.thumbnail_url && (lecture.media_sources ?? []).length > 0 ? ( + <div + style={{ + backgroundImage: `url('${lecture.thumbnail_url}')`, + }} + className="col-sm-2 col-12 thumbnailimg" + > + <Link href={`/${course_id_string ?? lecture.course_id}/${lecture.id}`}> + <span + aria-hidden="true" + className={"playpreviewbtn bi bi-play-circle"} + /> + </Link> + </div> + ) : ( + <div className="col-sm-2 col-12"></div> + )} <ul className="list-unstyled col-sm-3 col-12"> <li> <EmbeddedOMFieldComponent @@ -230,19 +254,11 @@ export function LectureListItem({ )} </li> ))} - - {/*% if ismod() %} - <li>{{ moderator_editor(['lectures',lecture.id,'internal'], lecture.internal) }}</li> - <li>Sichtbar: {{ moderator_checkbox(['lectures',lecture.id,'visible'], lecture.visible) }}</li> - <li>Livestream geplant: {{ moderator_checkbox(['lectures',lecture.id,'live'], lecture.live) }}</li> - <li>Wird nicht aufgenommen: {{ moderator_checkbox(['lectures',lecture.id,'norecording'], lecture.norecording) }}</li> - <li>Hörsaal: {{ moderator_editor(['lectures',lecture.id,'place'], lecture.place) }} </li> - {% endif %*/} </ul> <ul className="col-sm-4 col-12 list-unstyled d-flex"> {showDownloadSourcesButton && ( <DownloadSources - media_sources={lecture.media_sources} + media_sources={lecture.media_sources ?? []} direction="down" tabIndex="-1" /> @@ -251,10 +267,18 @@ export function LectureListItem({ <EditSources media_sources={lecture.media_sources} /> )} - <li className="flex-fill" /> - <li> + <li className="flex-fill mx-1" /> + <li className="p-1"> {editMode && ( <> + <EmbeddedOMFieldComponent + object_type="lecture" + object_id={lecture.id!} + field_id="is_visible" + field_type="boolean" + initialValue={lecture.is_visible} + changeIndicator="background" + /> <OMHistory object_type="lecture" object_id={lecture.id!} /> <OMEdit object_type="lecture" object_id={lecture.id!} /> <OMDelete object_type="lecture" object_id={lecture.id!} /> @@ -265,58 +289,14 @@ export function LectureListItem({ /> </li> </ul> - </> - ); - } else { - content = ( - <> - <div className="col-sm-2 col-12"></div> - <ul className="list-unstyled col-sm-3 col-12"> - <li> - <EmbeddedOMFieldComponent - object_type="lecture" - object_id={lecture.id!} - field_id="title" - field_type="string" - initialValue={lecture.title} - />{" "} - <LectureLiveLabel lecture={lecture} /> - </li> - </ul> - <ul className="list-unstyled col-sm-3 col-12"> - <li> - <EmbeddedOMFieldComponent - object_type="lecture" - object_id={lecture.id!} - field_id="time" - field_type="datetime" - initialValue={lecture.time} - /> - </li> - </ul> - <ul className="list-inline col-sm-4 col-12"> - <li> - <EmbeddedOMFieldComponent - object_type="lecture" - object_id={lecture.id!} - field_id="description" - field_type="string" - initialValue={lecture.description} - allowMarkdown={true} - /> - </li> - </ul> - </> - ); - } - - return ( - <li - className={`list-group-item highlighting-list-item ${lecture.no_recording ? "text-muted" : ""}`} - id={`lecture-${lecture.id}`} - {...props} - > - <div className="row">{content}</div> + </div> + {lecture.is_visible === false && ( + <> + <small className="text-muted"> + {language.get("ui.generic.object_invisible")} + </small> + </> + )} </li> ); } diff --git a/src/components/OMConfigComponent.tsx b/src/components/OMConfigComponent.tsx index 0fc46e604c029463d00ddb6950542260d528a813..e8781dd4735f7fd02bf7f376deee4b0ff155de6e 100644 --- a/src/components/OMConfigComponent.tsx +++ b/src/components/OMConfigComponent.tsx @@ -115,6 +115,7 @@ export function EmbeddedOMFieldComponent({ allowMarkdown = false, inline = false, className = "", + changeIndicator = "vertical-bar", }: { object_type: string; object_id: int; @@ -124,6 +125,7 @@ export function EmbeddedOMFieldComponent({ allowMarkdown?: boolean; inline?: boolean; className?: string; + changeIndicator?: "vertical-bar" | "background"; }) { const api = useBackendContext(); const reloadFunc = useReloadBoundary(); @@ -299,8 +301,14 @@ export function EmbeddedOMFieldComponent({ return ( <span className={ - "h-auto d-inline-flex align-middle " + className + ` ${isEditing ? "w-100" : ""}` + "h-auto d-inline-flex align-middle" + + className + + ` ${isEditing ? "w-100" : ""} ${changeIndicator === "background" ? "p-2 rounded" : ""}` } + style={{ + backgroundColor: + changeIndicator === "background" && hasChanged ? "rgba(255, 165, 0, 0.25)" : "", + }} title={title} onBlur={disableEditing} onSubmit={disableEditing} @@ -308,10 +316,13 @@ export function EmbeddedOMFieldComponent({ {isEditing && hasChanged && ( <StopNavigation warningText={language.get("ui.object.unsaved_changes")} /> )} - <div - className={"vr mx-1 " + (hasChanged ? "opacity-25" : "opacity-0")} - style={{ color: "orange", width: "4px", verticalAlign: "baseline" }} - /> + + {changeIndicator === "vertical-bar" && ( + <div + className={"vr mx-1 " + (hasChanged ? "opacity-25" : "opacity-0")} + style={{ color: "orange", width: "4px", verticalAlign: "baseline" }} + /> + )} {editor} {!isInstantEdit && ( <button diff --git a/src/components/Player.tsx b/src/components/Player.tsx index ce0a0ba6cc3ec9c80b39e5ee6c79e3a2feccf12a..e0d4156dd6c5d0d12d94286b3a01562cd4bdbe12 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -516,7 +516,7 @@ function Chapters({ chapters, seekTo }: { chapters?: chapter[]; seekTo: (time: n {chapters .sort((a, b) => a.start_time - b.start_time) .map((c) => { - const bgColor = c.is_visible === false ? "bg-danger-subtle" : ""; + const bgColor = c.is_visible === false ? "bg-danger-subtle" : "bg-body"; return ( <tr key={`${c.start_time}+${c.name}`} @@ -544,6 +544,7 @@ function Chapters({ chapters, seekTo }: { chapters?: chapter[]; seekTo: (time: n field_type="boolean" initialValue={c.is_visible} className="me-2 align-self-center" + changeIndicator="background" /> <OMDelete object_type="chapter" object_id={c.id!} /> </td> @@ -566,7 +567,9 @@ function VideoSuggestions({ course, lecture }: { course: course; lecture: lectur return ( <div className="d-flex w-100 flex-column flex-sm-row"> {prevLecture && ( - <div className="card"> + <div + className={`card ${prevLecture.is_visible === false ? "bg-danger-subtle" : ""}`} + > <Link className="card-header text-center bg-light-subtle" href={urlForLecture(course.id_string, prevLecture.id)} @@ -577,12 +580,19 @@ function VideoSuggestions({ course, lecture }: { course: course; lecture: lectur <div className="card-body"> <VideoCard course={course} lecture={prevLecture} size="small" /> </div> + {prevLecture.is_visible === false && ( + <div className="card-footer text-center text-muted"> + {language.get("ui.generic.object_invisible")} + </div> + )} </div> )} <div className="flex-fill p-2" /> {nextLecture && ( - <div className="card"> + <div + className={`card ${nextLecture.is_visible === false ? "bg-danger-subtle" : ""}`} + > <Link className="card-header text-center bg-light-subtle" href={urlForLecture(course.id_string, nextLecture.id)} @@ -593,6 +603,11 @@ function VideoSuggestions({ course, lecture }: { course: course; lecture: lectur <div className="card-body"> <VideoCard course={course} lecture={nextLecture} size="small" /> </div> + {nextLecture.is_visible === false && ( + <div className="card-footer text-center text-muted"> + {language.get("ui.generic.object_invisible")} + </div> + )} </div> )} </div> @@ -955,7 +970,7 @@ export default function Player({ playerData }: { playerData: ResourceType<Player /> </Head> <Title title={`${course.short_name} - ${lecture.title}`} /> - <div className="card"> + <div className={`card ${lecture.is_visible === false ? "bg-danger-subtle" : ""}`}> <div className="card-header d-flex"> <div className="flex-fill align-self-center"> <strong> @@ -985,6 +1000,14 @@ export default function Player({ playerData }: { playerData: ResourceType<Player <div> {editMode && ( <> + <EmbeddedOMFieldComponent + object_type="lecture" + object_id={lecture.id!} + field_id="is_visible" + field_type="boolean" + initialValue={lecture.is_visible} + changeIndicator="background" + /> <OMHistory object_type="lecture" object_id={lecture.id} /> <OMEdit object_type="lecture" object_id={lecture.id} /> <OMDelete object_type="lecture" object_id={lecture.id} /> @@ -1007,6 +1030,15 @@ export default function Player({ playerData }: { playerData: ResourceType<Player <div className="row mb-2">{pageContent}</div> <VideoSuggestions course={course} lecture={lecture} /> + + {lecture.is_visible === false && ( + <> + <hr /> + <small className="text-muted"> + {language.get("ui.generic.object_invisible")} + </small> + </> + )} </div> </div> </> diff --git a/src/misc/Util.tsx b/src/misc/Util.tsx index cf7102bd6785c832e6c37e8b7d5c2b2192cf3834..79057c08163382e6d75d31ed8f80ec9689c499dd 100644 --- a/src/misc/Util.tsx +++ b/src/misc/Util.tsx @@ -82,7 +82,7 @@ export function AuthenticationMethodIcons({ ); } return ( - <> + <div> {authentication_methods.map((method) => { let icon = undefined; let description = ""; @@ -129,7 +129,7 @@ export function AuthenticationMethodIcons({ </span> ); })} - </> + </div> ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e9da8aa69397fd72dab1b6838f6ec1efb52e24fa..50a5c630d8ee2ed2b6f4cbb2f2882eda10ce1cd4 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -24,6 +24,7 @@ import { LectureLiveLabel } from "@/components/LiveLabel"; function FeatureCard({ data, all_featured }: { data: featured; all_featured: featured[] }) { const { editMode } = useEditMode(); + const { language } = useLanguage(); const reloadFunc = useReloadBoundary(); const api = useBackendContext(); @@ -119,7 +120,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea <> <hr /> <small className="text-muted"> - This panel is invisible to regular users + {language.get("ui.generic.object_invisible")} </small> </> )} diff --git a/src/styles/globals.scss b/src/styles/globals.scss index cc0c97373a3792c7152f2e0387c5fd07a390a016..84f9decd0b80036bee5aca22bc19918a2f2cba85 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -89,6 +89,11 @@ $link-decoration: none; } } +// table td, tr +.table> :not(caption)>*>* { + background-color: inherit; // this fixes tables messing up background colors of parent elements +} + .icon-rwth { display: inline-block; vertical-align: middle;