Compare commits
3 Commits
8e2300bf53
...
1cd1584bb3
| Author | SHA1 | Date |
|---|---|---|
|
|
1cd1584bb3 | 1 year ago |
|
|
599560ca7d | 1 year ago |
|
|
9938dcc3bc | 1 year ago |
@ -1,52 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import MediaScroller from './MediaScroller.vue'
|
|
||||||
import { ContinueWatching } from '@/feed'
|
|
||||||
import type { FeedItem } from '@/feed'
|
|
||||||
|
|
||||||
const Items = (): FeedItem[] => {
|
|
||||||
// const feed = []
|
|
||||||
// jfapi
|
|
||||||
// .GetNextUp('http://192.168.0.63:30013')
|
|
||||||
// .then((data) => {
|
|
||||||
// for (const item of data.Items) {
|
|
||||||
// const thumbtag = item.ImageTags.Thumb ? item.ImageTags.Thumb : ''
|
|
||||||
// let summary = ''
|
|
||||||
// jfapi.GetItem('http://192.168.0.63:30013', '', item.Id).then((item) => {
|
|
||||||
// summary = item.Overview !== undefined ? item.Overview : ''
|
|
||||||
// })
|
|
||||||
// feed.push({
|
|
||||||
// title: item.SeriesName,
|
|
||||||
// subtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
|
|
||||||
// image: jfapi.ThumbImageUrl('http://192.168.0.63:30013', item.Id, thumbtag),
|
|
||||||
// summary: summary,
|
|
||||||
// infoSubtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
|
|
||||||
// imdbRating: item.CommunityRating,
|
|
||||||
// runtime: getDisplayDuration(item.RunTimeTicks),
|
|
||||||
// date: item.ProductionYear,
|
|
||||||
// MPAA: item.OfficialRating,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// return feed
|
|
||||||
// })
|
|
||||||
// .catch((err) => {
|
|
||||||
// return err
|
|
||||||
// })
|
|
||||||
return ContinueWatching
|
|
||||||
}
|
|
||||||
|
|
||||||
// function getDisplayDuration(ticks: number) {
|
|
||||||
// const totalMinutes = Math.round(ticks / 600000000) || 1
|
|
||||||
// const totalHours = Math.floor(totalMinutes / 60)
|
|
||||||
// const remainderMinutes = totalMinutes % 60
|
|
||||||
// const result = []
|
|
||||||
// if (totalHours > 0) {
|
|
||||||
// result.push(`${totalHours}h`)
|
|
||||||
// }
|
|
||||||
// result.push(`${remainderMinutes}m`)
|
|
||||||
// return result.join(' ')
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<MediaScroller title="Continue Watching" landscape :items="Items()" />
|
|
||||||
</template>
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { View } from '@/jfapi'
|
||||||
|
import NextUp from './NextUp.vue'
|
||||||
|
import LatestMedia from './LatestMedia.vue'
|
||||||
|
import { onBeforeMount, ref } from 'vue'
|
||||||
|
|
||||||
|
// const props = defineProps<{
|
||||||
|
// views: View[]
|
||||||
|
// }>()
|
||||||
|
|
||||||
|
const views = ref<View[]>()
|
||||||
|
const viewstring = ref<string | null>('')
|
||||||
|
views.value = undefined
|
||||||
|
onBeforeMount(() => {
|
||||||
|
viewstring.value = localStorage.getItem('jf_views')
|
||||||
|
views.value = JSON.parse(viewstring.value ? viewstring.value : '')
|
||||||
|
})
|
||||||
|
console.log('HomeMedia =>', views.value)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<NextUp />
|
||||||
|
<!-- <MediaScroller title="Latest Movies" :items="LatestMovies" />
|
||||||
|
<MediaScroller title="Latest Anime" :items="LatestAnime" /> -->
|
||||||
|
<LatestMedia v-for="view of views" :view="view" :key="view.Id" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import jfapi from '@/jfapi'
|
||||||
|
import MediaScroller from './MediaScroller.vue'
|
||||||
|
import type { FeedItem } from '@/feed'
|
||||||
|
import type { View } from '@/jfapi'
|
||||||
|
import { getDisplayDuration, getImageLink } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
view: View
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const Items = async (): Promise<FeedItem[]> => {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const feed: FeedItem[] = []
|
||||||
|
const userid = localStorage.getItem('jf_userid')
|
||||||
|
const data = await jfapi.GetLatest(userid ? userid : '', props.view)
|
||||||
|
if (data !== null) {
|
||||||
|
console.log(data)
|
||||||
|
for (const item of data) {
|
||||||
|
const summary = ''
|
||||||
|
if (userid === null) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
// const fullitem = await jfapi.GetItem(userid, item.Id)
|
||||||
|
// if (fullitem === null) {
|
||||||
|
// continue
|
||||||
|
// } else {
|
||||||
|
// summary = fullitem.Overview
|
||||||
|
// }
|
||||||
|
feed.push({
|
||||||
|
title: item.SeriesName || item.Name,
|
||||||
|
subtext: item.SeriesName
|
||||||
|
? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`
|
||||||
|
: String(item.ProductionYear),
|
||||||
|
image: getImageLink(item),
|
||||||
|
summary: summary,
|
||||||
|
// infoSubtext: item.SeriesName
|
||||||
|
// ? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`
|
||||||
|
// : fullitem.OriginalTitle != item.Name
|
||||||
|
// ? fullitem.OriginalTitle
|
||||||
|
// : '',
|
||||||
|
infoSubtext: '',
|
||||||
|
imdbRating: String(item.CommunityRating),
|
||||||
|
runtime: getDisplayDuration(item.RunTimeTicks),
|
||||||
|
date: String(item.ProductionYear),
|
||||||
|
MPAA: item.OfficialRating,
|
||||||
|
tag:
|
||||||
|
item.UserData?.UnplayedItemCount !== undefined
|
||||||
|
? String(item.UserData.UnplayedItemCount)
|
||||||
|
: '',
|
||||||
|
itemId: item.Id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('LatestMedia.vue =>', feed)
|
||||||
|
resolve(feed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MediaScroller :title="`Latest ${view?.Name}`" :feed-items="Items()" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FeedItem } from '@/feed'
|
||||||
|
import jfapi, { type View } from '@/jfapi'
|
||||||
|
import { getImageLink, getDisplayDuration } from './utils'
|
||||||
|
import { onBeforeMount, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import ScrollerItemPortrait from './ScrollerItemPortrait.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const viewId = String(route.params.id)
|
||||||
|
|
||||||
|
console.log(viewId)
|
||||||
|
|
||||||
|
const Items = async (viewId: string, type: string): Promise<FeedItem[]> => {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const feed: FeedItem[] = []
|
||||||
|
const userid = localStorage.getItem('jf_userid')
|
||||||
|
const data = await jfapi.GetItemsInView(userid ? userid : '', viewId, type)
|
||||||
|
if (data !== null) {
|
||||||
|
console.log(data)
|
||||||
|
for (const item of data.Items) {
|
||||||
|
const summary = ''
|
||||||
|
if (userid === null) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
feed.push({
|
||||||
|
title: item.SeriesName || item.Name,
|
||||||
|
subtext: item.SeriesName
|
||||||
|
? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`
|
||||||
|
: String(item.ProductionYear),
|
||||||
|
image: getImageLink(item),
|
||||||
|
summary: summary,
|
||||||
|
infoSubtext: '',
|
||||||
|
imdbRating: String(item.CommunityRating),
|
||||||
|
runtime: getDisplayDuration(item.RunTimeTicks),
|
||||||
|
date: String(item.ProductionYear),
|
||||||
|
MPAA: item.OfficialRating,
|
||||||
|
tag:
|
||||||
|
item.UserData?.UnplayedItemCount !== undefined
|
||||||
|
? String(item.UserData.UnplayedItemCount)
|
||||||
|
: '',
|
||||||
|
itemId: item.Id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('MediaGrid.vue =>', feed)
|
||||||
|
resolve(feed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const items = ref<FeedItem[]>()
|
||||||
|
const views = ref<View[]>()
|
||||||
|
const viewstring = ref<string | null>('')
|
||||||
|
views.value = undefined
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
viewstring.value = localStorage.getItem('jf_views')
|
||||||
|
views.value = JSON.parse(viewstring.value ? viewstring.value : '')
|
||||||
|
let viewType = ''
|
||||||
|
for (const view of views.value) {
|
||||||
|
if (view.Id === viewId) {
|
||||||
|
if (view.CollectionType.toLowerCase() === 'movies') {
|
||||||
|
viewType = 'Movie'
|
||||||
|
} else if (view.CollectionType.toLowerCase() === 'tvshows') {
|
||||||
|
viewType = 'Series'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('ItemType =>', viewType)
|
||||||
|
const data = ref<FeedItem[]>()
|
||||||
|
console.log(viewType)
|
||||||
|
data.value = await Items(viewId, viewType)
|
||||||
|
items.value = data.value
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="media-grid">
|
||||||
|
<div class="grid-wrapper">
|
||||||
|
<ScrollerItemPortrait
|
||||||
|
v-for="item in items"
|
||||||
|
:title="item.title"
|
||||||
|
:subtext="item.subtext"
|
||||||
|
:image="item.image"
|
||||||
|
:info-subtext="item.infoSubtext"
|
||||||
|
:date="item.date"
|
||||||
|
:runtime="item.runtime"
|
||||||
|
:summary="item.summary"
|
||||||
|
:-m-p-a-a="item.MPAA"
|
||||||
|
:imdb-rating="item.imdbRating"
|
||||||
|
:key="item.title"
|
||||||
|
:itemId="item.itemId"
|
||||||
|
:tag="item.tag"
|
||||||
|
class="scroller-item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.media-grid {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
.grid-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="info-misc">
|
||||||
|
<div class="misc-info-year">2019</div>
|
||||||
|
<div class="misc-info-runtime">2h 12m</div>
|
||||||
|
<div class="misc-info imdb-rating">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-star" size="xs" class="star-rating" /> 7.6
|
||||||
|
</div>
|
||||||
|
<div class="misc-info-age-rating"><span class="age-rating">R</span></div>
|
||||||
|
<div class="media-showcase-end-time">Ends at 6:23pm</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.info-misc {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: 3em;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import jfapi from '@/jfapi'
|
||||||
|
import MediaScroller from './MediaScroller.vue'
|
||||||
|
import type { FeedItem } from '@/feed'
|
||||||
|
import type { Item } from '@/jfapi'
|
||||||
|
|
||||||
|
const Items = async (): Promise<FeedItem[]> => {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const feed: FeedItem[] = []
|
||||||
|
const data = await jfapi.GetNextUp()
|
||||||
|
if (data !== null) {
|
||||||
|
for (const item of data.Items) {
|
||||||
|
const summary = ''
|
||||||
|
const userid = localStorage.getItem('jf_userid')
|
||||||
|
if (userid === null) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
// const fullitem = await jfapi.GetItem(userid ? userid : '', item.Id)
|
||||||
|
// if (fullitem === null) {
|
||||||
|
// summary = 'Failed to fetch content.'
|
||||||
|
// } else {
|
||||||
|
// summary = fullitem.Overview
|
||||||
|
// }
|
||||||
|
feed.push({
|
||||||
|
title: item.SeriesName !== undefined ? item.SeriesName : '',
|
||||||
|
subtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
|
||||||
|
image: getImageLink(item),
|
||||||
|
summary: summary,
|
||||||
|
infoSubtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
|
||||||
|
imdbRating: String(item.CommunityRating),
|
||||||
|
runtime: getDisplayDuration(item.RunTimeTicks),
|
||||||
|
date: String(item.ProductionYear),
|
||||||
|
MPAA: item.OfficialRating,
|
||||||
|
itemId: item.Id,
|
||||||
|
tag: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('NextUp.vue =>', feed)
|
||||||
|
resolve(feed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageLink(item: Item): string {
|
||||||
|
if (item.ParentThumbItemId === undefined) {
|
||||||
|
// using backdrop
|
||||||
|
return jfapi.BackdropImageUrl(
|
||||||
|
item.ParentBackdropItemId ? item.ParentBackdropItemId : '',
|
||||||
|
item.ParentBackdropImageTags ? item.ParentBackdropImageTags[0] : '',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// using thumb
|
||||||
|
return jfapi.ThumbImageUrl(
|
||||||
|
item.ParentThumbItemId,
|
||||||
|
item.ParentThumbImageTag ? item.ParentThumbImageTag : '',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayDuration(ticks: number) {
|
||||||
|
const totalMinutes = Math.round(ticks / 600000000) || 1
|
||||||
|
const totalHours = Math.floor(totalMinutes / 60)
|
||||||
|
const remainderMinutes = totalMinutes % 60
|
||||||
|
const result = []
|
||||||
|
if (totalHours > 0) {
|
||||||
|
result.push(`${totalHours}h`)
|
||||||
|
}
|
||||||
|
result.push(`${remainderMinutes}m`)
|
||||||
|
return result.join(' ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MediaScroller title="Next Up" landscape :feed-items="Items()" />
|
||||||
|
</template>
|
||||||
@ -1,87 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import jfapi from '@/jfapi'
|
||||||
|
import type { Item } from '@/jfapi'
|
||||||
|
|
||||||
|
function getImageLink(item: Item): string {
|
||||||
|
if (item.SeriesId === undefined) {
|
||||||
|
// using movie primary
|
||||||
|
return jfapi.PrimaryImageUrl(item.Id, item.ImageTags.Primary ? item.ImageTags.Primary : '')
|
||||||
|
} else {
|
||||||
|
// using series primary
|
||||||
|
return jfapi.PrimaryImageUrl(
|
||||||
|
item.SeriesId,
|
||||||
|
item.SeriesPrimaryImageTag ? item.SeriesPrimaryImageTag[0] : '',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayDuration(ticks: number) {
|
||||||
|
const totalMinutes = Math.round(ticks / 600000000) || 1
|
||||||
|
const totalHours = Math.floor(totalMinutes / 60)
|
||||||
|
const remainderMinutes = totalMinutes % 60
|
||||||
|
const result = []
|
||||||
|
if (totalHours > 0) {
|
||||||
|
result.push(`${totalHours}h`)
|
||||||
|
}
|
||||||
|
result.push(`${remainderMinutes}m`)
|
||||||
|
return result.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getImageLink, getDisplayDuration }
|
||||||
@ -1,29 +1,22 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import type { User, View } from '@/jfapi'
|
||||||
type UserData = {
|
|
||||||
host?: string
|
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
views?: [View]
|
|
||||||
}
|
|
||||||
type View = {
|
|
||||||
name: string
|
|
||||||
mediaType: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDataStore = defineStore('data', () => {
|
export const useDataStore = defineStore('data', () => {
|
||||||
const data = ref<UserData>({})
|
const data = ref<User>({
|
||||||
function setServerHost(host: string) {
|
Name: '',
|
||||||
data.value.host = host
|
Host: '',
|
||||||
}
|
Id: '',
|
||||||
function setViews(views: [View]) {
|
})
|
||||||
data.value.views = views
|
// function setServerHost(host: string) {
|
||||||
}
|
// data.value.Host = host
|
||||||
function setUser(id: string, name: string) {
|
// }
|
||||||
data.value.id = id
|
function setViews(views: View[]) {
|
||||||
data.value.name = name
|
data.value.Views = views
|
||||||
}
|
}
|
||||||
return { data, setServerHost, setViews, setUser }
|
// function setUser(id: string, name: string) {
|
||||||
|
// data.value.Id = id
|
||||||
|
// data.value.Name = name
|
||||||
|
// }
|
||||||
|
return { setViews }
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MediaShowcase from '@/components/MediaShowcase.vue'
|
||||||
|
import type { View, ViewList } from '@/jfapi'
|
||||||
|
import { onBeforeMount, ref } from 'vue'
|
||||||
|
import jfapi from '@/jfapi'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const views = ref<View[]>()
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
const viewlist = ref<ViewList | null>()
|
||||||
|
const userid = localStorage.getItem('jf_userid')
|
||||||
|
const host = localStorage.getItem('jf_host')
|
||||||
|
console.log('Host =>', host)
|
||||||
|
viewlist.value = await jfapi.LoadViews(userid ? userid : '')
|
||||||
|
if (window.location.pathname.toLowerCase() !== '/login') {
|
||||||
|
if (host === null) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
} else if (viewlist.value === null) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localStorage.setItem('jf_views', JSON.stringify(viewlist.value ? viewlist.value.Items : ''))
|
||||||
|
views.value = viewlist.value ? viewlist.value.Items : undefined
|
||||||
|
console.log(views.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const homeIsActive = () => {
|
||||||
|
return route.path === '/home'
|
||||||
|
}
|
||||||
|
const viewIsActive = (id: string) => {
|
||||||
|
return route.path.split('/')[2] === id
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<MediaShowcase />
|
||||||
|
<div class="home-nav">
|
||||||
|
<RouterLink to="/home" class="home-nav-item" :class="homeIsActive() ? 'active' : ''"
|
||||||
|
>Home</RouterLink
|
||||||
|
>
|
||||||
|
<RouterLink
|
||||||
|
class="home-nav-item"
|
||||||
|
v-for="view in views"
|
||||||
|
:to="`/view/${view.Id}`"
|
||||||
|
:key="view.Id"
|
||||||
|
:class="viewIsActive(view.Id) ? 'active' : ''"
|
||||||
|
>{{ view.Name }}</RouterLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="media">
|
||||||
|
<RouterView v-slot="{ Component, route }"
|
||||||
|
><component :is="Component" :key="route.path"
|
||||||
|
/></RouterView>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.home-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
gap: 3em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.home-nav-item:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-faded);
|
||||||
|
}
|
||||||
|
.home-nav-item.active {
|
||||||
|
padding: 0.4em 1.5em;
|
||||||
|
background-color: var(--color-text);
|
||||||
|
border-radius: 15px;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.home-nav-item.active:hover {
|
||||||
|
background-color: var(--color-text-faded);
|
||||||
|
}
|
||||||
|
.media {
|
||||||
|
margin: 0 10%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MediaInfo from '@/components/MediaInfo.vue'
|
||||||
|
import jfapi, { type Item } from '@/jfapi'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const itemId = String(route.params.id)
|
||||||
|
|
||||||
|
const item = ref<Item>()
|
||||||
|
const directors = ref<string[]>([])
|
||||||
|
const writers = ref<string[]>([])
|
||||||
|
// const studios = ref<string[]>()
|
||||||
|
const numAudioStreams = ref(0)
|
||||||
|
const userid = localStorage.getItem('jf_userid')
|
||||||
|
jfapi.GetItem(userid ? userid : '', itemId).then((data) => {
|
||||||
|
if (data === null) {
|
||||||
|
// error
|
||||||
|
} else {
|
||||||
|
item.value = data
|
||||||
|
}
|
||||||
|
if (item.value !== undefined) {
|
||||||
|
for (const stream of item.value.MediaStreams) {
|
||||||
|
if (stream.Type.toLowerCase() === 'audio') {
|
||||||
|
numAudioStreams.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const person of item.value.People) {
|
||||||
|
if (person.Type.toLowerCase() === 'director') {
|
||||||
|
directors.value.push(person.Name)
|
||||||
|
} else if (person.Type.toLowerCase() === 'writer') {
|
||||||
|
writers.value.push(person.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(writers.value, directors.value)
|
||||||
|
})
|
||||||
|
const arrayRange = (start: number, stop: number) =>
|
||||||
|
Array.from({ length: (stop - start) / 1 + 1 }, (value, index) => start + index * 1)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
:style="{
|
||||||
|
backgroundAttachment: 'fixed',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundImage: `url(${jfapi.FullBackdropImageUrl(item ? item.Id : '', item ? item.BackdropImageTags[0] : '', 0)})`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="logo">
|
||||||
|
<img :src="`${jfapi.LogoImageUrl(item.Id, item.ImageTags.Logo)}`" alt="" />
|
||||||
|
<MediaInfo />
|
||||||
|
</div>
|
||||||
|
<div class="info-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<div class="detail-buttons">
|
||||||
|
<div class="play-button">
|
||||||
|
<button class="primary-button">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-play" size="md" />Play Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="user-buttons">
|
||||||
|
<div class="watched">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-check" size="xl" class="clickable" />
|
||||||
|
</div>
|
||||||
|
<div class="favorited">
|
||||||
|
<font-awesome-icon :icon="['far', 'heart']" size="xl" class="clickable" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-streams">
|
||||||
|
<div class="stream-type">Video</div>
|
||||||
|
<div class="stream-value">{{ item?.MediaStreams[0].DisplayTitle }}</div>
|
||||||
|
<div class="stream-type">Audio</div>
|
||||||
|
<div class="stream-selector" v-if="numAudioStreams > 1">
|
||||||
|
<select name="audio-stream-select" :id="`${item?.Id}-audio-select`">
|
||||||
|
<option
|
||||||
|
:value="`${item?.MediaStreams}`"
|
||||||
|
v-for="i in arrayRange(1, numAudioStreams)"
|
||||||
|
:key="`audio-stream-${i}`"
|
||||||
|
>
|
||||||
|
{{ item?.MediaStreams[i].DisplayTitle }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="stream-value" v-else>{{ item?.MediaStreams[1].DisplayTitle }}</div>
|
||||||
|
<div class="stream-type">Subtitles</div>
|
||||||
|
<div class="stream-selector">
|
||||||
|
<select
|
||||||
|
name="subtitle-stream-select"
|
||||||
|
:id="`${item?.Id}-subtitle-select`"
|
||||||
|
v-if="item?.MediaStreams.length > numAudioStreams + 1"
|
||||||
|
>
|
||||||
|
<option value="Off">Off</option>
|
||||||
|
<option
|
||||||
|
:value="`${item?.MediaStreams}`"
|
||||||
|
v-for="i in arrayRange(numAudioStreams + 1, item.MediaStreams.length - 1)"
|
||||||
|
:key="`subtitle-stream-${i}`"
|
||||||
|
>
|
||||||
|
{{ item?.MediaStreams[i].DisplayTitle }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="stream-value" v-else>None</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<blockquote class="tagline" v-if="item?.Taglines.length > 0">
|
||||||
|
{{ item?.Taglines[0] }}
|
||||||
|
</blockquote>
|
||||||
|
<div class="overview">
|
||||||
|
{{ item?.Overview }}
|
||||||
|
</div>
|
||||||
|
<div class="misc-info">
|
||||||
|
<div class="info-type">Genre</div>
|
||||||
|
<div class="info-value">{{ item?.Genres.join(', ') }}</div>
|
||||||
|
<div class="info-type">{{ 'Director' + (directors.length > 1 ? 's' : '') }}</div>
|
||||||
|
<div class="info-value">{{ directors.join(', ') }}</div>
|
||||||
|
<div class="info-type">{{ 'Writer' + (writers.length > 1 ? 's' : '') }}</div>
|
||||||
|
<div class="info-value">{{ writers.join(', ') }}</div>
|
||||||
|
<div class="info-type">{{ 'Studio' + (item?.Studios.length > 1 ? 's' : '') }}</div>
|
||||||
|
<div class="info-value">{{ item?.Studios.map((s) => s.Name).join(', ') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.content-wrapper {
|
||||||
|
width: 95%;
|
||||||
|
margin: auto;
|
||||||
|
padding-top: 3em;
|
||||||
|
}
|
||||||
|
.item-info {
|
||||||
|
padding: 3em 5em;
|
||||||
|
width: 900px;
|
||||||
|
min-height: 80vh;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.info-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 3em;
|
||||||
|
width: 95%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.logo img {
|
||||||
|
max-height: 230px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.detail-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
.user-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
.media-streams {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.5fr 4fr;
|
||||||
|
}
|
||||||
|
.stream-type {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0.2em;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
color: var(--color-text-faded);
|
||||||
|
}
|
||||||
|
.stream-value {
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
.stream-selector {
|
||||||
|
margin: 0.2em;
|
||||||
|
}
|
||||||
|
.stream-selector select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.misc-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.5fr 4fr;
|
||||||
|
}
|
||||||
|
.misc-info .info-type {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
color: var(--color-text-faded);
|
||||||
|
}
|
||||||
|
.tagline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tagline::before {
|
||||||
|
content: '\201C';
|
||||||
|
font-size: 156px;
|
||||||
|
position: absolute;
|
||||||
|
top: -80px;
|
||||||
|
left: -20px;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: -1;
|
||||||
|
font-family: 'Georgia', Times, serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in new issue