forked from jellyfin/jellyfin-vue
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
392 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
<template> | ||
<VSlideYReverseTransition> | ||
<VBtn | ||
v-if="show" | ||
ref="btn" | ||
variant="outlined" | ||
:size="props.large ? 'large' : 'small'" | ||
:style="btnStyle" | ||
class="skip-segment" | ||
@click="skipSegment()"> | ||
Skip {{ segment.Type }} | ||
<template | ||
v-if="props.large" | ||
#append> | ||
<VIcon size="32"> | ||
<IMdiSkipNext /> | ||
</VIcon> | ||
</template> | ||
</VBtn> | ||
</VSlideYReverseTransition> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import IMdiSkipNext from 'virtual:icons/mdi/skip-next'; | ||
import { computed, shallowRef, watch, nextTick } from 'vue'; | ||
import { playbackManagerStore } from '@/store'; | ||
import { MediaSegment } from 'types/global/mediaSegment'; | ||
import { ticksToMs } from '@/utils/time'; | ||
// Props | ||
const props = defineProps<{ | ||
segment: MediaSegment; | ||
large?: boolean; | ||
}>(); | ||
const playbackManager = playbackManagerStore(); | ||
const btn = shallowRef<HTMLElement>(); | ||
/* | ||
* Vars | ||
* Show button seconds before (Segment.Start - 2) | ||
*/ | ||
const SHOW_START_OFFSET = 2; | ||
// Hide button seconds after (Segment.Start + 8) | ||
const SHOW_END_OFFSET = 8; | ||
const runtime = computed( | ||
() => ticksToMs(playbackManager.currentItem?.RunTimeTicks) / 1000 | ||
); | ||
const show = computed((): boolean => { | ||
if (!props.segment) { | ||
return false; | ||
} | ||
return ( | ||
(playbackManager?.currentTime || 0) >= | ||
Math.max(props.segment.Start - SHOW_START_OFFSET, 0) && | ||
(playbackManager?.currentTime || 0) <= | ||
Math.min(props.segment.Start + SHOW_END_OFFSET, runtime.value) | ||
); | ||
}); | ||
watch(show, (now) => { | ||
if (now) { | ||
// Force focus | ||
nextTick(() => btn?.value?.$el?.focus()); | ||
} | ||
}); | ||
const btnStyle = computed(() => { | ||
return { | ||
bottom: props.large ? '19%' : '8%' }; | ||
}); | ||
/** | ||
* | ||
*/ | ||
function skipSegment (): void { | ||
playbackManager.currentTime = props.segment.End - 2; // Additional -2s to prevent skipped content | ||
} | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
.skip-segment { | ||
overflow: hidden; | ||
background: rgb(0 0 0 / 28%); | ||
z-index: 9999; | ||
left: 3%; | ||
position: absolute; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
<template> | ||
<SegmentSkipButton | ||
v-for="segment in segmentsWithPrompt" | ||
:key="segment.ItemId + segment.Type + segment.TypeIndex" | ||
:segment="segment" | ||
:large="playerElement.isFullscreenVideoPlayer" /> | ||
<SegmentOverlay | ||
v-for="segment in segmentsWithoutPrompt" | ||
:key="segment.ItemId + segment.Type + segment.TypeIndex" | ||
:segment="segment" | ||
:large="playerElement.isFullscreenVideoPlayer" /> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { computed, ref, watch , nextTick } from 'vue'; | ||
import { AxiosRequestConfig } from 'axios'; | ||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; | ||
import { playbackManagerStore, playerElementStore } from '@/store'; | ||
import { MediaSegment } from 'types/global/mediaSegment'; | ||
import { useRemote } from '@/composables'; | ||
import RemotePluginAxiosInstance from '@/plugins/remote/axios'; | ||
const remote = useRemote(); | ||
const playbackManager = playbackManagerStore(); | ||
const playerElement = playerElementStore(); | ||
const currentSegments = ref<MediaSegment[]>([]); | ||
const axios = RemotePluginAxiosInstance; | ||
enum MediaSegmentType { | ||
INTRO = 'Intro', | ||
OUTRO = 'Outro', | ||
PREVIEW = 'Preview', | ||
RECAP = 'Recap', | ||
COMMERCIAL = 'Commercial' | ||
} | ||
enum MediaSegmentAction { | ||
AUTO = 'Auto', | ||
NONE = 'None', | ||
SKIP = 'Skip', | ||
PROMPT = 'Prompt', | ||
MUTE = 'Mute' | ||
} | ||
/** | ||
* Get default action for segments by segment type if action is AUTO | ||
*/ | ||
function getDefaultActionforType (seg: MediaSegment) : MediaSegmentAction { | ||
if (seg.Action == MediaSegmentAction.AUTO){ | ||
switch (seg.Type) { | ||
case MediaSegmentType.INTRO: { | ||
return MediaSegmentAction.PROMPT; | ||
} | ||
case MediaSegmentType.OUTRO: { | ||
return MediaSegmentAction.PROMPT; | ||
} | ||
case MediaSegmentType.PREVIEW: { | ||
return MediaSegmentAction.PROMPT; | ||
} | ||
case MediaSegmentType.RECAP: { | ||
return MediaSegmentAction.PROMPT; | ||
} | ||
case MediaSegmentType.COMMERCIAL: { | ||
return MediaSegmentAction.SKIP; | ||
} | ||
default: { | ||
return MediaSegmentAction.AUTO; | ||
} | ||
} | ||
} else { | ||
return seg.Action; | ||
} | ||
} | ||
const segmentsWithPrompt = computed(() => | ||
currentSegments.value.filter((seg) => getDefaultActionforType(seg) == MediaSegmentAction.PROMPT) | ||
); | ||
const segmentsWithoutPrompt = computed(() => | ||
currentSegments.value.filter((seg) => getDefaultActionforType(seg) != MediaSegmentAction.PROMPT) | ||
); | ||
watch( | ||
() => playbackManager.currentItem, | ||
(item: BaseItemDto | undefined) => { | ||
void fetchMediaSegments(item); | ||
} | ||
); | ||
let fetchMediaSegments = async function ( | ||
item: BaseItemDto | undefined | ||
): Promise<void> { | ||
// Reset | ||
currentSegments.value = []; | ||
if (item === undefined) { | ||
return; | ||
} | ||
if ( | ||
item?.Type === BaseItemKind.Episode || | ||
item?.Type === BaseItemKind.Movie | ||
) { | ||
try { | ||
let token = remote.auth.currentUserToken; | ||
let userId = remote.auth.currentUserId; | ||
let config: AxiosRequestConfig = { | ||
headers: { Authorization: `MediaBrowser Token="${token}"` } | ||
}; | ||
const segmentsResponse = await axios.instance.get( | ||
`/MediaSegment?itemId=${item?.Id}&userId=${userId}`, | ||
config | ||
); | ||
// Inject Action | ||
for (const [ind, el] of segmentsResponse.data.Items.entries()) { | ||
segmentsResponse.data.Items[ind].Action = getDefaultActionforType(el); | ||
} | ||
currentSegments.value = segmentsResponse.data.Items; | ||
} catch (error) { | ||
console.error('Fetching segs', error); | ||
} | ||
} | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
<template> | ||
<VSlideYReverseTransition> | ||
<VBtn | ||
v-if="show" | ||
variant="outlined" | ||
:size="props.large ? 'large' : 'small'" | ||
:style="btnStyle" | ||
class="overlay-segment"> | ||
{{ btnText }} | ||
<template | ||
v-if="props.large" | ||
#prepend> | ||
<VIcon size="32"> | ||
<IMdiVolumeMute | ||
v-if="props.segment.Action == MediaSegmentAction.MUTE" /> | ||
<IMdiInformationOutline v-else /> | ||
</VIcon> | ||
</template> | ||
</VBtn> | ||
</VSlideYReverseTransition> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import IMdiInformationOutline from 'virtual:icons/mdi/information-outline'; | ||
import IMdiVolumeMute from 'virtual:icons/mdi/volume-mute'; | ||
import { computed, watch } from 'vue'; | ||
import { playbackManagerStore } from '@/store'; | ||
import { MediaSegment } from 'types/global/mediaSegment'; | ||
import { ticksToMs } from '@/utils/time'; | ||
// Props | ||
const props = defineProps<{ | ||
segment: MediaSegment; | ||
large?: boolean; | ||
}>(); | ||
const playbackManager = playbackManagerStore(); | ||
enum MediaSegmentType { | ||
INTRO = 'Intro', | ||
OUTRO = 'Outro', | ||
PREVIEW = 'Preview', | ||
RECAP = 'Recap', | ||
COMMERCIAL = 'Commercial' | ||
} | ||
enum MediaSegmentAction { | ||
AUTO = 'Auto', | ||
NONE = 'None', | ||
SKIP = 'Skip', | ||
PROMPT = 'Prompt', | ||
MUTE = 'Mute' | ||
} | ||
// Show message seconds before (Segment.Start - 0) | ||
const SHOW_START_OFFSET = 0; | ||
// Hide message seconds after (Segment.Start + 5) | ||
const SHOW_END_OFFSET = 7; | ||
// We execute and show just once | ||
let executed = false; | ||
const runtime = computed( | ||
() => ticksToMs(playbackManager.currentItem?.RunTimeTicks) / 1000 | ||
); | ||
const show = computed((): boolean => { | ||
if (!props.segment) { | ||
return false; | ||
} | ||
let state = false; | ||
if (props.segment.Action == MediaSegmentAction.MUTE && !executed) { | ||
state = | ||
(playbackManager?.currentTime || 0) >= | ||
Math.max(props.segment.Start - SHOW_START_OFFSET, 0) && | ||
(playbackManager?.currentTime || 0) <= | ||
Math.min(props.segment.Start + SHOW_END_OFFSET, runtime.value); | ||
} | ||
if (props.segment.Action == MediaSegmentAction.SKIP && !executed) { | ||
state = | ||
(playbackManager?.currentTime || 0) >= | ||
Math.max(props.segment.Start - SHOW_START_OFFSET, 0) && | ||
(playbackManager?.currentTime || 0) <= | ||
Math.min(props.segment.End + SHOW_END_OFFSET, runtime.value); | ||
} | ||
return state; | ||
}); | ||
watch(show, (val) => { | ||
if (val) { | ||
if (props.segment.Action == MediaSegmentAction.SKIP && !executed) { | ||
playbackManager.currentTime = props.segment.End - 2; // Prevent content skipped | ||
} | ||
if ( | ||
props.segment.Action == MediaSegmentAction.MUTE && | ||
!executed && | ||
!playbackManager.isMuted | ||
) { | ||
playbackManager.toggleMute(); | ||
} | ||
} else { | ||
if ( | ||
props.segment.Action == MediaSegmentAction.MUTE && | ||
playbackManager.isMuted | ||
) { | ||
playbackManager.toggleMute(); | ||
} | ||
// Mark as executed, when segment is hidden again | ||
executed = true; | ||
} | ||
}); | ||
const btnText = computed(() => { | ||
if (props.segment.Action == MediaSegmentAction.MUTE) { | ||
return `Mute ${props.segment.Type}`; | ||
} | ||
if (props.segment.Action == MediaSegmentAction.SKIP) { | ||
return `Skipped ${props.segment.Type}`; | ||
} | ||
}); | ||
const btnStyle = computed(() => { | ||
return { | ||
bottom: props.large ? '19%' : '8%' | ||
}; | ||
}); | ||
</script> | ||
|
||
<style> | ||
.overlay-segment { | ||
overflow: hidden; | ||
background: rgb(0 0 0 / 28%); | ||
z-index: 9999; | ||
left: 3%; | ||
position: absolute; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.