mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-18 07:02:27 +00:00
Merge pull request #409 from michielcx/feature/progress
feat: Episode Progress Tracking and Resume
This commit is contained in:
commit
8bb711a911
7 changed files with 119 additions and 3 deletions
|
|
@ -2,8 +2,9 @@
|
|||
import { statusColorMap } from '@/modules/anime.js'
|
||||
import EpisodePreviewCard from './EpisodePreviewCard.svelte'
|
||||
import { hoverClick } from '@/modules/click.js'
|
||||
import { since } from '@/modules/util'
|
||||
import { since } from '@/modules/util.js'
|
||||
import { getContext } from 'svelte'
|
||||
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
|
||||
export let data
|
||||
|
||||
let preview = false
|
||||
|
|
@ -19,6 +20,8 @@
|
|||
function setHoverState (state) {
|
||||
preview = state
|
||||
}
|
||||
|
||||
const progress = liveAnimeEpisodeProgress(media.id, data.episode)
|
||||
</script>
|
||||
|
||||
<div class='d-flex p-20 pb-10 position-relative' use:hoverClick={[data.onclick || viewMedia, setHoverState]}>
|
||||
|
|
@ -34,6 +37,11 @@
|
|||
{media.duration}m
|
||||
{/if}
|
||||
</div>
|
||||
{#if $progress > 0}
|
||||
<div class='progress container-fluid position-absolute' style='height: 2px; min-height: 2px;'>
|
||||
<div class='progress-bar' style='width: {$progress}%' />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='row pt-15'>
|
||||
<div class='col pr-10'>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<script>
|
||||
import { statusColorMap, formatMap } from '@/modules/anime.js'
|
||||
import { since } from '@/modules/util'
|
||||
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
|
||||
export let data
|
||||
|
||||
const media = data.media
|
||||
|
||||
const episodeThumbnail = ((!media?.mediaListEntry?.status || !(media.mediaListEntry.status === 'CURRENT' && media.mediaListEntry.progress < data.episode)) && data.episodeData?.image) || media?.bannerImage || media?.coverImage.extraLarge || ' '
|
||||
let hide = true
|
||||
|
||||
const progress = liveAnimeEpisodeProgress(media.id, data.episode)
|
||||
</script>
|
||||
|
||||
<div class='position-absolute w-400 mh-400 absolute-container top-0 m-auto bg-dark-light z-30 rounded overflow-hidden pointer d-flex flex-column'>
|
||||
|
|
@ -29,6 +32,11 @@
|
|||
{media.duration}m
|
||||
{/if}
|
||||
</div>
|
||||
{#if $progress > 0}
|
||||
<div class='progress container-fluid position-absolute mb-5'>
|
||||
<div class='progress-bar' style='width: {$progress}%' />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='w-full d-flex flex-column flex-grow-1 px-20 pb-15'>
|
||||
<div class='row pt-15'>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<img loading='lazy' src={media.coverImage.extraLarge || ''} alt='cover' class='cover-img w-full rounded' style:--color={media.coverImage.color || '#1890ff'} />
|
||||
|
||||
<div class='text-white font-weight-very-bold font-size-16 pt-15 title overflow-hidden'>
|
||||
{#if media.mediaListEntry?.status}
|
||||
<div style:--statusColor={statusColorMap[media.mediaListEntry.status]} class='list-status-circle d-inline-flex overflow-hidden mr-5' title={media.mediaListEntry.status} />
|
||||
|
|
|
|||
69
common/modules/animeprogress.js
Normal file
69
common/modules/animeprogress.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { writable, derived } from 'simple-store-svelte'
|
||||
|
||||
// Maximum number of entries to keep in LocalStorage
|
||||
const maxEntries = 1000
|
||||
|
||||
// LocalStorage is structured as an array of objects with the following properties:
|
||||
// mediaId, episode, currentTime, safeduration, createdAt, updatedAt
|
||||
function loadFromLocalStorage() {
|
||||
const data = localStorage.getItem('animeEpisodeProgress')
|
||||
return data ? JSON.parse(data) : []
|
||||
}
|
||||
|
||||
function saveToLocalStorage(data) {
|
||||
localStorage.setItem('animeEpisodeProgress', JSON.stringify(data))
|
||||
animeProgressStore.set(data)
|
||||
}
|
||||
|
||||
const animeProgressStore = writable(loadFromLocalStorage())
|
||||
|
||||
// Return an object with the progress of each episode in percent (0-100), keyed by episode number
|
||||
export function liveAnimeProgress (mediaId){
|
||||
return derived(animeProgressStore, (data) => {
|
||||
if (!mediaId) return {}
|
||||
const results = data.filter(item => item.mediaId === mediaId)
|
||||
if (!results) return {}
|
||||
// Return an object with the episode as the key and the progress as the value
|
||||
return Object.fromEntries(results.map(result => [
|
||||
result.episode,
|
||||
Math.ceil(result.currentTime / result.safeduration * 100)
|
||||
]))
|
||||
})
|
||||
}
|
||||
|
||||
// Return an individual episode's progress in percent (0-100)
|
||||
export function liveAnimeEpisodeProgress (mediaId, episode) {
|
||||
return derived(animeProgressStore, (data) => {
|
||||
if (!mediaId || !episode) return 0
|
||||
const result = data.find(item => item.mediaId === mediaId && item.episode === episode)
|
||||
if (!result) return 0
|
||||
return Math.ceil(result.currentTime / result.safeduration * 100)
|
||||
})
|
||||
}
|
||||
|
||||
// Return an individual episode's record { mediaId, episode, currentTime, safeduration, createdAt, updatedAt }
|
||||
export function getAnimeProgress(mediaId, episode) {
|
||||
const data = loadFromLocalStorage()
|
||||
return data.find(item => item.mediaId === mediaId && item.episode === episode)
|
||||
}
|
||||
|
||||
// Set an individual episode's progress
|
||||
export function setAnimeProgress({ mediaId, episode, currentTime, safeduration }) {
|
||||
if (!mediaId || !episode || !currentTime || !safeduration) return
|
||||
const data = loadFromLocalStorage()
|
||||
// Update the existing entry or create a new one
|
||||
const existing = data.find(item => item.mediaId === mediaId && item.episode === episode)
|
||||
if (existing) {
|
||||
existing.currentTime = currentTime
|
||||
existing.safeduration = safeduration
|
||||
existing.updatedAt = Date.now()
|
||||
} else {
|
||||
data.push({ mediaId, episode, currentTime, safeduration, createdAt: Date.now(), updatedAt: Date.now() })
|
||||
}
|
||||
// Remove the oldest entries if we have too many
|
||||
while (data.length > maxEntries) {
|
||||
const oldest = data.reduce((a, b) => a.updatedAt < b.updatedAt ? a : b)
|
||||
data.splice(data.indexOf(oldest), 1)
|
||||
}
|
||||
saveToLocalStorage(data)
|
||||
}
|
||||
|
|
@ -23,4 +23,4 @@
|
|||
"video-deband": "^1.0.5",
|
||||
"webpack-merge": "^5.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { settings } from '@/modules/settings.js'
|
||||
import { getAnimeProgress, setAnimeProgress } from '@/modules/animeprogress.js'
|
||||
import { playAnime } from '../RSSView.svelte'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
|
@ -173,6 +174,7 @@
|
|||
client.send('current', file)
|
||||
subs = new Subtitles(video, files, current, handleHeaders)
|
||||
video.load()
|
||||
await loadAnimeProgress()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +200,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function loadAnimeProgress () {
|
||||
if (!current?.media?.media?.id || !current.media.media.episode || current.media.failed || !media?.media?.id || !media.episode) return
|
||||
|
||||
const animeProgress = await getAnimeProgress(current.media.media.id, current.media.episode)
|
||||
if (!animeProgress) return
|
||||
|
||||
const currentTime = Math.max(animeProgress.currentTime - 5, 0) // Load 5 seconds before
|
||||
seek(currentTime - video.currentTime)
|
||||
}
|
||||
|
||||
function saveAnimeProgress () {
|
||||
if (!current?.media?.media?.id || !current.media.media.episode || current.media.failed || !media?.media?.id || !media.episode) return
|
||||
|
||||
if (buffering || paused || video.readyState < 4) return
|
||||
|
||||
setAnimeProgress({ mediaId: current.media.media.id, episode: current.media.episode, currentTime: video.currentTime, safeduration })
|
||||
}
|
||||
setInterval(saveAnimeProgress, 30000)
|
||||
|
||||
function cycleSubtitles () {
|
||||
if (current && subs?.headers) {
|
||||
const tracks = subs.headers.filter(header => header)
|
||||
|
|
@ -970,6 +991,7 @@
|
|||
on:loadedmetadata={autoPlay}
|
||||
on:loadedmetadata={checkAudio}
|
||||
on:loadedmetadata={clearLoadInterval}
|
||||
on:loadedmetadata={loadAnimeProgress}
|
||||
on:leavepictureinpicture={() => { pip = false }} />
|
||||
{#if stats}
|
||||
<div class='position-absolute top-0 bg-tp p-10 m-15 text-monospace rounded z-50'>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
import { since } from '@/modules/util'
|
||||
import { click } from '@/modules/click.js'
|
||||
import { getEpisodeNumberByAirDate } from '@/modules/providers/tosho.js'
|
||||
import { alRequest } from '@/modules/anilist'
|
||||
import { alRequest } from '@/modules/anilist.js'
|
||||
import { liveAnimeProgress } from '@/modules/animeprogress.js'
|
||||
|
||||
export let media
|
||||
|
||||
|
|
@ -40,11 +41,14 @@
|
|||
}
|
||||
}
|
||||
load()
|
||||
|
||||
const animeProgress = liveAnimeProgress(id)
|
||||
</script>
|
||||
|
||||
{#each episodeOrder ? episodeList : [...episodeList].reverse() as { episode, image, summary, rating, title, length, airdate }}
|
||||
{@const completed = userProgress >= episode}
|
||||
{@const target = userProgress + 1 === episode}
|
||||
{@const progress = $animeProgress?.[episode] ?? 0}
|
||||
<div class='w-full my-20 content-visibility-auto scale' class:opacity-half={completed} class:px-20={!target} class:h-150={image || summary} use:click={() => play(episode)}>
|
||||
<div class='rounded w-full h-full overflow-hidden d-flex flex-xsm-column flex-row pointer' class:border={target} class:bg-black={completed} class:bg-dark={!completed}>
|
||||
{#if image}
|
||||
|
|
@ -67,6 +71,10 @@
|
|||
<div class='progress mb-15' style='height: 2px; min-height: 2px;'>
|
||||
<div class='progress-bar w-full' />
|
||||
</div>
|
||||
{:else if progress}
|
||||
<div class='progress mb-15' style='height: 2px; min-height: 2px;'>
|
||||
<div class='progress-bar' style="width: {progress}%" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class='font-size-12 overflow-hidden'>
|
||||
{summary || ''}
|
||||
|
|
|
|||
Loading…
Reference in a new issue