feat: preserve fullscreen when autoplaying next episode
This commit is contained in:
@@ -37,6 +37,7 @@ func (h *PlaybackHandler) Register(r *gin.Engine) {
|
|||||||
r.GET("/anime/:id/watch", h.HandleWatchPage)
|
r.GET("/anime/:id/watch", h.HandleWatchPage)
|
||||||
r.POST("/api/watch-progress", h.HandleSaveProgress)
|
r.POST("/api/watch-progress", h.HandleSaveProgress)
|
||||||
r.POST("/api/watch-complete", h.HandleWatchComplete)
|
r.POST("/api/watch-complete", h.HandleWatchComplete)
|
||||||
|
r.GET("/api/watch/episode/:animeId/:episode", h.HandleEpisodeData)
|
||||||
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
|
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
|
||||||
r.GET("/watch/proxy/stream", h.HandleProxyStream)
|
r.GET("/watch/proxy/stream", h.HandleProxyStream)
|
||||||
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
|
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
|
||||||
@@ -78,6 +79,65 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "watch.gohtml", responseData)
|
c.HTML(http.StatusOK, "watch.gohtml", responseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleEpisodeData returns the minimal payload needed to advance to the next
|
||||||
|
// episode without a full page reload (preserves fullscreen).
|
||||||
|
func (h *PlaybackHandler) HandleEpisodeData(c *gin.Context) {
|
||||||
|
animeID, err := strconv.Atoi(c.Param("animeId"))
|
||||||
|
if err != nil || animeID <= 0 {
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
episode := c.Param("episode")
|
||||||
|
if episode == "" {
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := c.DefaultQuery("mode", "sub")
|
||||||
|
|
||||||
|
user, _ := c.Get("User")
|
||||||
|
userID := ""
|
||||||
|
if u, ok := user.(*domain.User); ok {
|
||||||
|
userID = u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.svc.BuildWatchData(c.Request.Context(), animeID, []string{}, episode, mode, userID)
|
||||||
|
if err != nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
watchData, _ := data["WatchData"].(map[string]any)
|
||||||
|
if watchData == nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modeSources := watchData["ModeSources"]
|
||||||
|
availableModes, _ := watchData["AvailableModes"].([]string)
|
||||||
|
segments := watchData["Segments"]
|
||||||
|
|
||||||
|
// Try to resolve a title for this episode from the episode list.
|
||||||
|
episodeTitle := ""
|
||||||
|
if eps, ok := watchData["Episodes"].([]domain.EpisodeData); ok {
|
||||||
|
epNum, _ := strconv.Atoi(episode)
|
||||||
|
for _, e := range eps {
|
||||||
|
if e.MalID == epNum {
|
||||||
|
episodeTitle = e.Title
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"mode_sources": modeSources,
|
||||||
|
"available_modes": availableModes,
|
||||||
|
"segments": segments,
|
||||||
|
"episode_title": episodeTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
||||||
user, _ := c.Get("User")
|
user, _ := c.Get("User")
|
||||||
userID := ""
|
userID := ""
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export const goToNextEpisode = async (): Promise<void> => {
|
|||||||
markEpisodeTransition(nextEp);
|
markEpisodeTransition(nextEp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/watch/episode/${state.malID}/${nextEp}`);
|
const res = await fetch(
|
||||||
|
`/api/watch/episode/${state.malID}/${nextEp}?mode=${encodeURIComponent(state.currentMode)}`
|
||||||
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// fallback: full page navigation
|
// fallback: full page navigation
|
||||||
sessionStorage.setItem('mal:autoplay-next', 'true');
|
sessionStorage.setItem('mal:autoplay-next', 'true');
|
||||||
@@ -53,12 +55,14 @@ export const goToNextEpisode = async (): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// load new video
|
// load new video (keep preferences)
|
||||||
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}`;
|
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
||||||
|
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
||||||
state.video.load();
|
state.video.load();
|
||||||
if (!state.video.paused) state.video.play().catch(() => {});
|
if (!state.video.paused) state.video.play().catch(() => {});
|
||||||
|
|
||||||
state.currentEpisode = String(nextEp);
|
state.currentEpisode = String(nextEp);
|
||||||
|
state.currentMode = fallback;
|
||||||
state.pendingSeekTime = null;
|
state.pendingSeekTime = null;
|
||||||
state.completionSent = false;
|
state.completionSent = false;
|
||||||
state.completionAttempts = 0;
|
state.completionAttempts = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user