From b45e9700d4ab2c314dcaf9494bf149f29d04a544 Mon Sep 17 00:00:00 2001
From: Dorian Koch <doriank@fsmpi.rwth-aachen.de>
Date: Thu, 4 Jul 2024 19:07:57 +0200
Subject: [PATCH] Show a list of media sources in edit mode, Add file size to
 media sources, Closes #42

---
 src/components/CourseListing.tsx   | 25 +++++++-----
 src/components/DownloadManager.tsx | 18 ++-------
 src/components/Player.tsx          | 65 ++++++++++++++++++++++++++----
 src/misc/Formatting.tsx            | 15 +++++++
 4 files changed, 92 insertions(+), 31 deletions(-)

diff --git a/src/components/CourseListing.tsx b/src/components/CourseListing.tsx
index 5a30790..2c8d00a 100644
--- a/src/components/CourseListing.tsx
+++ b/src/components/CourseListing.tsx
@@ -8,7 +8,7 @@ import {
     EmbeddedOMFieldComponent,
 } from "./OMConfigComponent";
 import { useEditMode } from "./EditModeProvider";
-import { DownloadSources } from "./Player";
+import { DownloadSources, EditSources } from "./Player";
 import Title from "./TitleComponent";
 import DownloadAllModal from "./DownloadManager";
 
@@ -126,10 +126,15 @@ export function LectureListItem({
 }) {
     const api = useBackendContext();
     const { editMode } = useEditMode();
+
     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);
 
         content = (
             <>
@@ -233,14 +238,16 @@ export function LectureListItem({
                     {% endif %*/}
                 </ul>
                 <ul className="col-sm-4 col-12 list-unstyled d-flex">
-                    {lecture.media_sources &&
-                        lecture.media_sources.some((ele) => ele.download_url !== undefined) && (
-                            <DownloadSources
-                                media_sources={lecture.media_sources}
-                                direction="down"
-                                tabIndex="-1"
-                            />
-                        )}
+                    {showDownloadSourcesButton && (
+                        <DownloadSources
+                            media_sources={lecture.media_sources}
+                            direction="down"
+                            tabIndex="-1"
+                        />
+                    )}
+                    {editMode && lecture.media_sources && (
+                        <EditSources media_sources={lecture.media_sources} />
+                    )}
 
                     <li className="flex-fill" />
                     <li>
diff --git a/src/components/DownloadManager.tsx b/src/components/DownloadManager.tsx
index 3e215ca..f7489d1 100644
--- a/src/components/DownloadManager.tsx
+++ b/src/components/DownloadManager.tsx
@@ -1,6 +1,6 @@
 import { useEffect, useRef, useState } from "react";
 import Modal from "react-bootstrap/Modal";
-import { sourceToName } from "./Player";
+import { sortSources, sourceToName } from "./Player";
 import { showWarningToast } from "@/misc/ErrorHandlers";
 
 import type { GetCourseResponse, media_source } from "@/api/api_v1_types";
@@ -311,13 +311,7 @@ export default function DownloadAllModal({ course }: { course: GetCourseResponse
         chosenQualities.current = new Map<number, string>();
         chosenLectures.current = new Map<number, boolean>();
         for (let lecture of course.lectures!) {
-            let sortedsources = Array.from(lecture.media_sources ?? [])
-                .filter((q) => q.download_url)
-                .sort((a, b) => {
-                    let aq = parseInt(a.quality.name.split("p")[0]);
-                    let bq = parseInt(b.quality.name.split("p")[0]);
-                    return bq - aq;
-                });
+            let sortedsources = sortSources(lecture.media_sources).filter((q) => q.download_url);
             chosenLectures.current.set(lecture.id, sortedsources.length > 0);
             if (sortedsources.length > 0)
                 chosenQualities.current.set(lecture.id, sourceToName(sortedsources[0]));
@@ -328,13 +322,7 @@ export default function DownloadAllModal({ course }: { course: GetCourseResponse
 
     let lectures = [];
     for (let lecture of course.lectures!) {
-        let sortedsources = Array.from(lecture.media_sources ?? [])
-            .filter((q) => q.download_url)
-            .sort((a, b) => {
-                let aq = parseInt(a.quality.name.split("p")[0]);
-                let bq = parseInt(b.quality.name.split("p")[0]);
-                return bq - aq;
-            });
+        let sortedsources = sortSources(lecture.media_sources).filter((q) => q.download_url);
 
         let selectedQuality: media_source | undefined = undefined;
         if (chosenQualities.current.get(lecture.id)) {
diff --git a/src/components/Player.tsx b/src/components/Player.tsx
index 341f532..37f582c 100644
--- a/src/components/Player.tsx
+++ b/src/components/Player.tsx
@@ -3,7 +3,7 @@ import "video.js/dist/video-js.min.css";
 import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css";
 import Link from "next/link";
 import { useBackendContext } from "./BackendProvider";
-import { timestampToString } from "@/misc/Formatting";
+import { filesizeToHuman, timestampToString } from "@/misc/Formatting";
 import OverlayTrigger from "react-bootstrap/OverlayTrigger";
 import Popover from "react-bootstrap/Popover";
 
@@ -518,11 +518,66 @@ function Chapters({ chapters, seekTo }: { chapters?: chapter[]; seekTo: (time: n
 }
 
 export function sourceToName(source: media_source) {
-    if (source.comment.length == 0) return source.quality.name;
+    if (source.comment.length == 0)
+        return `${source.quality.name} (${filesizeToHuman(source.size)})`;
 
     return `${source.quality.name} (${source.comment})`;
 }
 
+export function sortSources(sources?: media_source[]) {
+    return Array.from(sources ?? []).sort((a, b) => {
+        let aq = parseInt(a.quality.name.split("p")[0]);
+        let bq = parseInt(b.quality.name.split("p")[0]);
+        if (aq === bq) {
+            return (a.id ?? 0) - (b.id ?? 0);
+        }
+        return bq - aq;
+    });
+}
+
+export function EditSources({ media_sources }: { media_sources: media_source[] }) {
+    const { editMode } = useEditMode();
+
+    let sortedsources = sortSources(media_sources);
+
+    return (
+        <ul>
+            {sortedsources.map((source) => (
+                <li
+                    key={source.id}
+                    className={
+                        "d-flex border align-items-center px-2 py-1 rounded " +
+                        (source.is_visible === false ? "bg-danger-subtle" : "")
+                    }
+                >
+                    <span className="bi bi-download" />
+                    <a href={source.download_url ?? source.player_url} className="mx-1">
+                        {sourceToName(source)}
+                    </a>
+                    {editMode && (
+                        <>
+                            <div className="flex-fill" />
+                            <EmbeddedOMFieldComponent
+                                object_type="media_source"
+                                object_id={source.id!}
+                                field_id="is_visible"
+                                field_type="boolean"
+                                initialValue={source.is_visible}
+                                className="me-2 align-self-center py-0"
+                            />
+                            <OMDelete
+                                object_type="media_source"
+                                object_id={source.id!}
+                                className="py-0"
+                            />
+                        </>
+                    )}
+                </li>
+            ))}
+        </ul>
+    );
+}
+
 export function DownloadSources({
     media_sources,
     className,
@@ -537,11 +592,7 @@ export function DownloadSources({
     const { editMode } = useEditMode();
     const { language } = useLanguage();
 
-    let sortedsources = Array.from(media_sources ?? []).sort((a, b) => {
-        let aq = parseInt(a.quality.name.split("p")[0]);
-        let bq = parseInt(b.quality.name.split("p")[0]);
-        return bq - aq;
-    });
+    let sortedsources = sortSources(media_sources);
 
     return (
         <Dropdown drop={direction}>
diff --git a/src/misc/Formatting.tsx b/src/misc/Formatting.tsx
index 239b945..0657733 100644
--- a/src/misc/Formatting.tsx
+++ b/src/misc/Formatting.tsx
@@ -31,3 +31,18 @@ export function datetimeToString(datetime: string) {
     let date_parsed = new Date(datetime);
     return `${date_parsed.toLocaleDateString([], { weekday: "short", year: "numeric", month: "2-digit", day: "2-digit" })}, ${date_parsed.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
 }
+export function filesizeToHuman(size: number) {
+    if (size < 1024) {
+        return size + " B";
+    }
+    size /= 1024;
+    if (size < 1024) {
+        return size.toFixed(1) + " KiB";
+    }
+    size /= 1024;
+    if (size < 1024) {
+        return size.toFixed(1) + " MiB";
+    }
+    size /= 1024;
+    return size.toFixed(1) + " GiB";
+}
-- 
GitLab