work on item page

main
Gabe Farrell 1 year ago
parent 599560ca7d
commit 1cd1584bb3

Binary file not shown.

@ -1,5 +1,5 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { RouterLink, RouterView } from 'vue-router'
import { ref } from 'vue'
import jfapi from './jfapi'
const menuOpen = ref(false)
@ -19,7 +19,7 @@ const logout = () => {
<font-awesome-icon icon="fa-solid fa-bars" size="xl" />
</div>
<div class="menu" v-show="menuOpen">
<div class="menu-item">Home</div>
<RouterLink class="menu-item" to="/home">Home</RouterLink>
<div class="menu-item">Settings</div>
<div class="menu-item" @click="logout">Sign Out</div>
</div>
@ -27,7 +27,6 @@ const logout = () => {
<div class="wrapper">
<RouterView />
</div>
<div class="footer"></div>
</template>
<style scoped>
@ -59,14 +58,15 @@ const logout = () => {
align-items: baseline;
flex-direction: column;
gap: 1em;
z-index: 100; /* always on top */
}
.menu-item {
font-size: 14px;
}
.menu-item:hover {
color: var(--color-text-faded);
cursor: pointer;
text-decoration: none;
}
.footer {
height: 100px;

@ -20,8 +20,25 @@
} */
}
select {
padding: 0.5em 1em;
border-radius: 15px;
background: rgba(0, 0, 0, 0.1);
color: var(--color-text);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
border: 1px solid rgba(255, 255, 255, 0.3);
outline: none;
}
option {
background-color: var(--color-background-darker);
}
button.primary-button {
padding: 0.6em 1.5em;
padding: 0.8em 1.75em;
background-color: var(--color-text);
outline: none;
border: none;
@ -81,6 +98,15 @@ svg.active.clickable:hover {
cursor: pointer;
}
a {
text-decoration: none;
color: var(--color-text);
}
a:hover {
text-decoration: underline;
}
.clamp {
overflow: hidden;
display: -webkit-box;

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

@ -2,7 +2,8 @@
import jfapi from '@/jfapi'
import MediaScroller from './MediaScroller.vue'
import type { FeedItem } from '@/feed'
import type { Item, View } from '@/jfapi'
import type { View } from '@/jfapi'
import { getDisplayDuration, getImageLink } from './utils'
const props = defineProps<{
view: View
@ -55,31 +56,6 @@ const Items = async (): Promise<FeedItem[]> => {
}
})
}
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(' ')
}
</script>
<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>

@ -67,7 +67,7 @@ const disableForward = () => {
transform: `translateX(${translate}px)`,
}"
>
<div class="scroller-wrapper clearfix" v-if="landscape">
<div class="scroller-wrapper" v-if="landscape">
<ScrollerItemLandscape
v-for="item in items"
:title="item.title"
@ -84,7 +84,7 @@ const disableForward = () => {
class="scroller-item"
/>
</div>
<div class="scroller-wrapper clearfix" v-else>
<div class="scroller-wrapper" v-else>
<ScrollerItemPortrait
v-for="item in items"
:title="item.title"
@ -113,7 +113,6 @@ const disableForward = () => {
.scroller-wrapper {
display: flex;
gap: 1em;
font-size: 12px;
}
.scroller-controls svg {
padding: 1em;

@ -42,12 +42,14 @@ const props = defineProps({
},
})
const overview = ref('')
const seriesId = ref('')
const userid = localStorage.getItem('jf_userid')
jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
if (data === null) {
// error
} else {
overview.value = data.Overview
seriesId.value = String(data.SeriesId)
}
})
</script>
@ -71,7 +73,9 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
<div class="info-box">
<div class="info-box-content">
<div class="info-box-header">
<div class="info-box-title">{{ props.title }}</div>
<RouterLink class="info-box-title clamp" :to="`/item/${seriesId}`">{{
props.title
}}</RouterLink>
<div class="info-box-subtext">{{ props.infoSubtext }}</div>
</div>
<div class="info-box-summary clamp-4">{{ overview }}</div>
@ -104,6 +108,7 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 12px;
}
/* .scroller-item:hover {
cursor: pointer;
@ -210,6 +215,11 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
.info-box-title {
font-weight: 600;
font-size: 18px;
color: var(--color-text);
text-decoration: none;
}
.info-box-title:hover {
text-decoration: underline;
}
.info-box-subtext {
color: var(--color-text-faded);

@ -1,6 +1,7 @@
<script setup lang="ts">
import jfapi from '@/jfapi'
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
const props = defineProps({
title: {
@ -66,9 +67,11 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
<template>
<div class="scroller-item">
<div class="scroller-item-img">
<!-- <div class="scroller-item-poster-wrapper"> -->
<div class="poster-tag" v-if="props.tag !== '' && props.tag !== undefined">
{{ props.tag }}
<div class="text-tag" v-if="props.tag !== '0'">{{ props.tag }}</div>
<div class="icon-tag" v-else>
<font-awesome-icon icon="fa-solid fa-check" size="md" class="clickable" />
</div>
</div>
<font-awesome-icon icon="fa-solid fa-play" class="play-icon" />
<img
@ -79,11 +82,12 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
height: `${120 * (3 / 2)}px`,
}"
/>
<!-- </div> -->
<div class="info-box">
<div class="info-box-content">
<div class="info-box-header">
<div class="info-box-title clamp">{{ props.title }}</div>
<RouterLink class="info-box-title clamp" :to="`/item/${props.itemId}`">{{
props.title
}}</RouterLink>
<div class="info-box-subtext" v-show="infosubtxt !== ''">
{{ infosubtxt }}
</div>
@ -118,10 +122,8 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 12px;
}
/* .scroller-item:hover {
cursor: pointer;
} */
.play-icon {
position: absolute;
top: 75px;
@ -249,6 +251,11 @@ jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
.info-box-title {
font-weight: 600;
font-size: 18px;
color: var(--color-text);
text-decoration: none;
}
.info-box-title:hover {
text-decoration: underline;
}
.info-box-subtext {
color: var(--color-text-faded);

@ -1,90 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a>
and follow the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</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 }

@ -41,6 +41,7 @@ type Item = {
Name: string
OriginalTitle: string
Id: string
ParentId?: string
Type: string
OfficialRating: string
ImageTags: ImageTags
@ -52,12 +53,40 @@ type Item = {
SeriesId?: string
Overview: string
RunTimeTicks: number
BackdropImageTags: string[]
ParentThumbItemId?: string
ParentThumbImageTag?: string
ParentBackdropItemId?: string
ParentBackdropImageTags?: string[]
SeriesPrimaryImageTag?: string
UserData?: UserData
MediaStreams: MediaStream[]
Taglines: string[]
Genres: string[]
Studios: Studio[]
People: Person[]
}
type Person = {
Name: string
Id: string
Role: string
Type: string
PrimaryImageTag?: string
}
type Studio = {
Name: string
Id: string
}
type MediaStream = {
Language: string
Type: string
DisplayTitle: string
IsDefault: boolean
IsExternal: boolean
IsForced: boolean
}
type ItemList = {
@ -73,6 +102,7 @@ type UserData = {
type ImageTags = {
Primary?: string
Thumb?: string
Logo?: string
}
type User = {
@ -82,7 +112,7 @@ type User = {
Views?: View[]
}
export type { User, View, Item, ViewList, ItemList }
export type { User, View, Item, ViewList, ItemList, MediaStream }
class JFApi {
request<T>(endpoint: Endpoint): Promise<T | null> {
@ -128,9 +158,9 @@ class JFApi {
method: 'GET',
})
}
GetItemsInView(userid: string, viewId: string) {
return this.request({
resource: `http://192.168.0.63:30013/Users/${userid}/Items?SortBy=DateCreated%2CSortName%2CProductionYear&SortOrder=Descending&IncludeItemTypes=Movie&Recursive=true&Fields=PrimaryImageAspectRatio%2CMediaSourceCount&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=0&ParentId=${viewId}&Limit=100`,
GetItemsInView(userid: string, viewId: string, type: string) {
return this.request<ItemList>({
resource: `/Users/${userid}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=${type}&Recursive=true&Fields=PrimaryImageAspectRatio&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=0&Limit=100&ParentId=${viewId}`,
method: 'GET',
})
}
@ -142,10 +172,18 @@ class JFApi {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Backdrop?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
}
FullBackdropImageUrl(itemId: string, imgTag: string, imgIndex: number) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Backdrop/${imgIndex}?tag=${imgTag}`
}
PrimaryImageUrl(itemId: string, imgTag: string) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Primary?fillHeight=446&fillWidth=297&quality=96&tag=${imgTag}`
}
LogoImageUrl(itemId: string, imgTag: string) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Logo?tag=${imgTag}`
}
Login(host: string, username: string, password: string): Promise<User | null> {
return new Promise((resolve) => {
fetch(`${host}/Users/authenticatebyname`, {

@ -12,23 +12,12 @@ import { library } from '@fortawesome/fontawesome-svg-core'
/* import font awesome icon component */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
fetch('http://192.168.0.63:30013/Branding/Configuration')
.then((r) => r.json())
.then((r) => console.log(r))
/* import specific icons */
import {
faCheck,
faHeart,
faPlay,
faBars,
faEllipsis,
faStar,
faCircle,
} from '@fortawesome/free-solid-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
/* add icons to the library */
library.add(faCheck, faHeart, faPlay, faBars, faEllipsis, faStar, faCircle)
library.add(fas, far)
const app = createApp(App)

@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
import BrowseView from '@/views/BrowseView.vue'
import HomeMedia from '@/components/HomeMedia.vue'
import MediaGrid from '@/components/MediaGrid.vue'
import ItemView from '@/views/ItemView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -17,7 +19,17 @@ const router = createRouter({
},
{
path: 'view/:id',
component: HomeMedia,
component: MediaGrid,
},
],
},
{
path: '/item',
redirect: '/home',
children: [
{
path: ':id',
component: ItemView,
},
],
},

@ -3,6 +3,7 @@ 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 () => {
@ -22,22 +23,45 @@ onBeforeMount(async () => {
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">
<div class="home-nav-item active">Home</div>
<div class="home-nav-item" v-for="view in views" :key="view.Id">{{ view.Name }}</div>
<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 />
<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;

@ -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…
Cancel
Save