diff --git a/api/stream.go b/api/stream.go index b50174cad..82b83bb0a 100644 --- a/api/stream.go +++ b/api/stream.go @@ -12,13 +12,13 @@ import ( "strings" "time" - "github.com/getsentry/sentry-go" - "github.com/gin-gonic/gin" "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" "github.com/TUM-Dev/gocast/tools" "github.com/TUM-Dev/gocast/tools/bot" "github.com/TUM-Dev/gocast/voice-service/pb" + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -496,6 +496,8 @@ func (r streamRoutes) createVideoSectionBatch(c *gin.Context) { log.WithError(err).Error("failed to generate video section images") } }() + + c.JSON(http.StatusOK, sections) } type UpdateVideoSectionRequest struct { diff --git a/dao/courses.go b/dao/courses.go index 23978d086..18294dfea 100644 --- a/dao/courses.go +++ b/dao/courses.go @@ -201,6 +201,7 @@ func (d coursesDao) GetCourseByToken(token string) (course model.Course, err err func (d coursesDao) GetCourseById(ctx context.Context, id uint) (course model.Course, err error) { var foundCourse model.Course dbErr := DB.Preload("Streams.TranscodingProgresses"). + Preload("Streams.VideoSections"). Preload("Streams.Files"). Preload("Streams", func(db *gorm.DB) *gorm.DB { return db.Order("streams.start desc") @@ -218,7 +219,7 @@ func (d coursesDao) GetCourseBySlugYearAndTerm(ctx context.Context, slug string, return cachedCourses.(model.Course), nil } var course model.Course - err := DB.Preload("Streams.Units", func(db *gorm.DB) *gorm.DB { + err := DB.Preload("Streams.VideoSections").Preload("Streams.Units", func(db *gorm.DB) *gorm.DB { return db.Order("unit_start desc") }).Preload("Streams", func(db *gorm.DB) *gorm.DB { return db.Order("start desc") diff --git a/model/stream.go b/model/stream.go index 6fac079af..35de2d675 100755 --- a/model/stream.go +++ b/model/stream.go @@ -323,6 +323,18 @@ func (s Stream) getJson(lhs []LectureHall, course Course) gin.H { } } + var videoSections []gin.H + for _, section := range s.VideoSections { + videoSections = append(videoSections, gin.H{ + "id": section.ID, + "description": section.Description, + "startHours": section.StartHours, + "startMinutes": section.StartMinutes, + "startSeconds": section.StartSeconds, + "fileID": section.FileID, + }) + } + return gin.H{ "lectureId": s.Model.ID, "courseId": s.CourseID, @@ -346,6 +358,7 @@ func (s Stream) getJson(lhs []LectureHall, course Course) gin.H { "courseSlug": course.Slug, "private": s.Private, "downloadableVods": s.GetVodFiles(), + "videoSections": videoSections, } } diff --git a/web/assets/init-admin.js b/web/assets/init-admin.js index 172d2513d..f4e8be806 100644 --- a/web/assets/init-admin.js +++ b/web/assets/init-admin.js @@ -15,6 +15,8 @@ document.addEventListener("alpine:init", () => { 'color' ]; + const nativeEventName = "csupdate"; + const convert = (modifiers, value) => { if (modifiers.includes("int")) { return parseInt(value); @@ -79,7 +81,6 @@ document.addEventListener("alpine:init", () => { Alpine.directive("bind-change-set", (el, { expression, value, modifiers }, { evaluate, cleanup }) => { const changeSet = evaluate(expression); const fieldName = value || el.name; - const nativeEventName = "csupdate"; if (el.type === "file") { const isSingle = modifiers.includes("single") @@ -92,7 +93,7 @@ document.addEventListener("alpine:init", () => { if (!data[fieldName]) { el.value = ""; } - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); @@ -109,7 +110,7 @@ document.addEventListener("alpine:init", () => { const onChangeSetUpdateHandler = (data) => { el.checked = !!data[fieldName]; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: !!data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: !!data[fieldName] }})); }; changeSet.listen(onChangeSetUpdateHandler); @@ -122,26 +123,29 @@ document.addEventListener("alpine:init", () => { }) } else if (el.tagName === "textarea" || textInputTypes.includes(el.type)) { const keyupHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); + const changeHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); const onChangeSetUpdateHandler = (data) => { el.value = `${data[fieldName]}`; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); - el.addEventListener('keyup', keyupHandler) + el.addEventListener('keyup', keyupHandler); + el.addEventListener('change', changeHandler); el.value = `${changeSet.get()[fieldName]}`; cleanup(() => { changeSet.removeListener(onChangeSetUpdateHandler); el.removeEventListener('keyup', keyupHandler) + el.removeEventListener('change', changeHandler) }) } else { const changeHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); const onChangeSetUpdateHandler = (data) => { el.value = `${data[fieldName]}`; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); @@ -154,4 +158,94 @@ document.addEventListener("alpine:init", () => { }) } }); + + /** + * Alpine.js directive for dynamically triggering a custom event and updating an element's inner text + * based on changes to a "change set" object's field. + * + * Syntax: + *
+ * + * Parameters: + * - changeSetExpression: The JavaScript expression evaluating to the change set object + * - fieldName: The specific field within the change set to monitor for changes + * + * Modifiers: + * - "text": When provided, the directive will also update the element's innerText. + * + * Custom Events: + * - "csupdate": Custom event triggered when the change set is updated. + * The detail property of the event object contains the new value of the specified field. + */ + Alpine.directive("change-set-listen", (el, { expression, modifiers }, { effect, evaluate, cleanup }) => { + effect(() => { + const [changeSetExpression, fieldName = null] = expression.split("."); + const changeSet = evaluate(changeSetExpression); + + const onChangeSetUpdateHandler = (data) => { + const value = fieldName != null ? data[fieldName] : data; + if (modifiers.includes("text")) { + el.innerText = `${value}`; + } + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value } })); + }; + + if (!changeSet) { + return; + } + + changeSet.removeListener(onChangeSetUpdateHandler); + onChangeSetUpdateHandler(changeSet.get()); + changeSet.listen(onChangeSetUpdateHandler); + + cleanup(() => { + changeSet.removeListener(onChangeSetUpdateHandler); + }) + }); + }); + + /** + * Alpine.js directive for executing custom logic in response to the "csupdate" event, + * which is usually triggered by changes in a "change set" object's field. + * + * Syntax: + *
+ * + * Parameters: + * - expression: The JavaScript expression to be evaluated when the "csupdate" event is triggered. + * + * Modifiers: + * - "init": When provided, the directive will execute the expression during initialization (no matter if its dirty or clean). + * - "clean": When provided, the directive will only execute if changeSet is not dirty. + * - "dirty": When provided, the directive will only execute if changeSet is dirty. + * + * Example usage: + *
+ *
+ */ + Alpine.directive("on-change-set-update", (el, { expression, modifiers }, { evaluate, evaluateLater, cleanup }) => { + const onUpdate = evaluateLater(expression); + + const onChangeSetUpdateHandler = (e) => { + const isDirty = e.detail.changeSet.isDirty(); + + if (modifiers.includes("clean") && isDirty) { + return; + } + if (modifiers.includes("dirty") && !isDirty) { + return; + } + onUpdate(); + }; + el.addEventListener(nativeEventName, onChangeSetUpdateHandler); + + if (modifiers.includes("init")) { + evaluate(expression); + } + + cleanup(() => { + el.removeEventListener(nativeEventName, onChangeSetUpdateHandler); + }) + }) }); \ No newline at end of file diff --git a/web/template/partial/course/manage/course_settings.gohtml b/web/template/partial/course/manage/course_settings.gohtml index 1994b0ce3..66fb7b6c9 100644 --- a/web/template/partial/course/manage/course_settings.gohtml +++ b/web/template/partial/course/manage/course_settings.gohtml @@ -5,7 +5,7 @@ }">
+
Video Sections
@@ -29,147 +27,131 @@
-
- - Unsaved Changes -
- +
: : -
- -