Creates /channel page

This commit is contained in:
Arhey 2023-02-17 15:01:46 +03:00
parent 677365c6da
commit 33d6a13667
6 changed files with 456 additions and 347 deletions

View file

@ -1,151 +1,157 @@
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
import StreamsPopup from './StreamsPopup.svelte' import StreamsPopup from './StreamsPopup.svelte'
import GuidesPopup from './GuidesPopup.svelte' import GuidesPopup from './GuidesPopup.svelte'
import ChannelPopup from './ChannelPopup.svelte' import ChannelPopup from './ChannelPopup.svelte'
import { search, query, hasQuery, setSearchParam } from '../store.js' import { search, query, hasQuery, setSearchParam } from '../store.js'
export let channel export let channel
const guides = channel._guides const guides = channel._guides
const streams = channel._streams const streams = channel._streams
const { open } = getContext('simple-modal') const currLocation = window.location.href
const showGuides = () => const { open } = getContext('simple-modal')
open( const onOpened = () => {
GuidesPopup, window.history.pushState({}, `${channel.name} • iptv-org`, `/channel?id=${channel.id}`)
{ guides, title: channel.name }, }
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } } const onClosed = () => {
) window.history.pushState({}, `iptv-org`, currLocation)
const showStreams = () => }
open( const showGuides = () =>
StreamsPopup, open(
{ streams, title: channel.name }, GuidesPopup,
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } } { guides, title: channel.name },
) { transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
const showChannelData = () => { )
open( const showStreams = () =>
ChannelPopup, open(
{ channel }, StreamsPopup,
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } } { streams, title: channel.name },
) { transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
} )
const showChannelData = () => {
open(
ChannelPopup,
{ channel },
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } },
{ onOpened, onClosed }
)
}
function pluralize(number, word) { function pluralize(number, word) {
return number > 1 ? word + 's' : word return number > 1 ? word + 's' : word
} }
function searchBy(q) { function searchBy(q) {
if ($query !== q) { if ($query !== q) {
query.set(q) query.set(q)
hasQuery.set(true) hasQuery.set(true)
search(q) search(q)
setSearchParam('q', q) setSearchParam('q', q)
} }
close() close()
} }
</script> </script>
<tr <tr
class="border-b last:border-b-0 border-gray-200 dark:border-gray-700 hover:bg-gray-50 hover:dark:bg-gray-700 h-16" class="border-b last:border-b-0 border-gray-200 dark:border-gray-700 hover:bg-gray-50 hover:dark:bg-gray-700 h-16"
> >
<td class="pl-2 pr-4 md:pr-7 py-2"> <td class="pl-2 pr-4 md:pr-7 py-2">
<div class="inline-flex w-full align-middle justify-center whitespace-nowrap overflow-hidden"> <div class="inline-flex w-full align-middle justify-center whitespace-nowrap overflow-hidden">
{#if channel.logo} {#if channel.logo}
<img <img
class="block align-middle mx-auto max-w-[6rem] max-h-[3rem] text-sm text-gray-400 dark:text-gray-600 cursor-default" class="block align-middle mx-auto max-w-[6rem] max-h-[3rem] text-sm text-gray-400 dark:text-gray-600 cursor-default"
loading="lazy" loading="lazy"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
src="{channel.logo}" src="{channel.logo}"
alt="{channel.name}" alt="{channel.name}"
/> />
{/if} {/if}
</div> </div>
</td> </td>
<td class="pl-3 pr-2 py-2"> <td class="pl-3 pr-2 py-2">
<div> <div>
<div class="text-left"> <div class="text-left">
<a <a
on:click|preventDefault="{showChannelData}" on:click|preventDefault="{showChannelData}"
href="/" href="/channel?id={channel.id}"
rel="nofollow" tabindex="0"
role="button" class="font-normal text-gray-600 dark:text-white hover:underline hover:text-blue-500"
tabindex="0" >
class="font-normal text-gray-600 dark:text-white hover:underline hover:text-blue-500" {channel.name}
> </a>
{channel.name} {#if channel._searchable.is === 'closed'}
</a> <div
{#if channel._searchable.is === 'closed'} class="text-gray-500 border-[1px] border-gray-200 text-xs inline-flex items-center px-2.5 py-0.5 ml-1 mr-2 dark:text-gray-300 cursor-default rounded-full"
<div title="closed: {channel.closed}"
class="text-gray-500 border-[1px] border-gray-200 text-xs inline-flex items-center px-2.5 py-0.5 ml-1 mr-2 dark:text-gray-300 cursor-default rounded-full" >
title="closed: {channel.closed}" Closed
> </div>
Closed {/if} {#if channel.alt_names.length}
</div> <div class="text-sm text-gray-400 dark:text-gray-400">{channel.alt_names.join(', ')}</div>
{/if} {#if channel.alt_names.length} {/if}
<div class="text-sm text-gray-400 dark:text-gray-400">{channel.alt_names.join(', ')}</div> </div>
{/if} </div>
</div> </td>
</div> <td class="px-2 py-2">
</td> <div>
<td class="px-2 py-2"> <code
<div> class="break-words text-sm text-gray-600 bg-gray-100 dark:text-gray-300 dark:bg-gray-700 px-2 py-1 rounded-sm select-all cursor-text font-mono"
<code >{channel.id}</code
class="break-words text-sm text-gray-600 bg-gray-100 dark:text-gray-300 dark:bg-gray-700 px-2 py-1 rounded-sm select-all cursor-text font-mono" >
>{channel.id}</code </div>
> </td>
</div> <td class="pl-2 pr-5 py-2">
</td> <div class="text-right flex justify-end space-x-3 items-center">
<td class="pl-2 pr-5 py-2"> {#if guides.length}
<div class="text-right flex justify-end space-x-3 items-center"> <button
{#if guides.length} on:click="{showGuides}"
<button class="text-sm text-gray-500 dark:text-gray-100 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400"
on:click="{showGuides}" >
class="text-sm text-gray-500 dark:text-gray-100 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400" <svg
> xmlns="http://www.w3.org/2000/svg"
<svg viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 20" class="w-5 h-5"
fill="currentColor" >
class="w-5 h-5" <path
> d="M5.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H6a.75.75 0 01-.75-.75V12zM6 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H6zM7.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H8a.75.75 0 01-.75-.75V12zM8 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H8zM9.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V10zM10 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H10zM9.25 14a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V14zM12 9.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V10a.75.75 0 00-.75-.75H12zM11.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H12a.75.75 0 01-.75-.75V12zM12 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H12zM13.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H14a.75.75 0 01-.75-.75V10zM14 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H14z"
<path />
d="M5.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H6a.75.75 0 01-.75-.75V12zM6 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H6zM7.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H8a.75.75 0 01-.75-.75V12zM8 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H8zM9.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V10zM10 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H10zM9.25 14a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V14zM12 9.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V10a.75.75 0 00-.75-.75H12zM11.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H12a.75.75 0 01-.75-.75V12zM12 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H12zM13.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H14a.75.75 0 01-.75-.75V10zM14 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H14z" <path
/> fill-rule="evenodd"
<path d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z"
fill-rule="evenodd" clip-rule="evenodd"
d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" />
clip-rule="evenodd" </svg>
/>
</svg>
<div>{guides.length}</div> <div>{guides.length}</div>
<div>{pluralize(guides.length, 'guide')}</div> <div>{pluralize(guides.length, 'guide')}</div>
</button> </button>
{/if}{#if streams.length} {/if}{#if streams.length}
<button <button
on:click="{showStreams}" on:click="{showStreams}"
class="text-sm text-gray-500 dark:text-gray-100 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400" class="text-sm text-gray-500 dark:text-gray-100 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"
/> />
</svg> </svg>
<div>{streams.length}</div> <div>{streams.length}</div>
<div>{pluralize(streams.length, 'stream')}</div> <div>{pluralize(streams.length, 'stream')}</div>
</button> </button>
{/if} {/if}
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -1,168 +1,163 @@
<script> <script>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { search, query, hasQuery, channels, setSearchParam } from '../store.js' import { goto } from '$app/navigation'
import { search, query, hasQuery, channels, setSearchParam } from '~/store'
export let data export let data
export let close export let close
let replaced_by = null let replaced_by = null
if (data.replaced_by) { if (data.replaced_by) {
const channel = $channels.find(c => c.id === data.replaced_by) const channel = $channels.find(c => c.id === data.replaced_by)
if (channel) replaced_by = channel.name if (channel) replaced_by = channel.name
} }
const fieldset = [ const fieldset = [
{ name: 'logo', type: 'image', value: data.logo }, { name: 'logo', type: 'image', value: data.logo },
{ name: 'id', type: 'string', value: data.id }, { name: 'id', type: 'string', value: data.id },
{ name: 'name', type: 'string', value: data.name }, { name: 'name', type: 'string', value: data.name },
{ name: 'alt_names', type: 'string', value: data.alt_names.join(', ') }, { name: 'alt_names', type: 'string', value: data.alt_names.join(', ') },
{ {
name: 'network', name: 'network',
type: 'link', type: 'link',
value: data.network ? { label: data.network, query: `network:${norm(data.network)}` } : null value: data.network ? { label: data.network, query: `network:${norm(data.network)}` } : null
}, },
{ {
name: 'owners', name: 'owners',
type: 'link[]', type: 'link[]',
value: data.owners.map(value => ({ label: value, query: `owners:${norm(value)}` })) value: data.owners.map(value => ({ label: value, query: `owners:${norm(value)}` }))
}, },
{ {
name: 'country', name: 'country',
type: 'link', type: 'link',
value: { label: data._country.name, query: `country:${data._country.code}` } value: { label: data._country.name, query: `country:${data._country.code}` }
}, },
{ {
name: 'subdivision', name: 'subdivision',
type: 'link', type: 'link',
value: data._subdivision value: data._subdivision
? { label: data._subdivision.name, query: `subdivision:${data._subdivision.code}` } ? { label: data._subdivision.name, query: `subdivision:${data._subdivision.code}` }
: null : null
}, },
{ {
name: 'city', name: 'city',
type: 'link', type: 'link',
value: data.city ? { label: data.city, query: `city:${norm(data.city)}` } : null value: data.city ? { label: data.city, query: `city:${norm(data.city)}` } : null
}, },
{ {
name: 'broadcast_area', name: 'broadcast_area',
type: 'link[]', type: 'link[]',
value: data._broadcast_area.map(v => ({ value: data._broadcast_area.map(v => ({
label: v.name, label: v.name,
query: `broadcast_area:${v.type}/${v.code}` query: `broadcast_area:${v.type}/${v.code}`
})) }))
}, },
{ {
name: 'languages', name: 'languages',
type: 'link[]', type: 'link[]',
value: data._languages.map(v => ({ label: v.name, query: `languages:${v.code}` })) value: data._languages.map(v => ({ label: v.name, query: `languages:${v.code}` }))
}, },
{ {
name: 'categories', name: 'categories',
type: 'link[]', type: 'link[]',
value: data._categories.map(v => ({ label: v.name, query: `categories:${v.id}` })) value: data._categories.map(v => ({ label: v.name, query: `categories:${v.id}` }))
}, },
{ {
name: 'is_nsfw', name: 'is_nsfw',
type: 'link', type: 'link',
value: { label: data.is_nsfw.toString(), query: `is_nsfw:${data.is_nsfw.toString()}` } value: { label: data.is_nsfw.toString(), query: `is_nsfw:${data.is_nsfw.toString()}` }
}, },
{ {
name: 'launched', name: 'launched',
type: 'date', type: 'date',
value: data.launched ? dayjs(data.launched).format('D MMMM YYYY') : null value: data.launched ? dayjs(data.launched).format('D MMMM YYYY') : null
}, },
{ {
name: 'closed', name: 'closed',
type: 'date', type: 'date',
value: data.closed ? dayjs(data.closed).format('D MMMM YYYY') : null value: data.closed ? dayjs(data.closed).format('D MMMM YYYY') : null
}, },
{ name: 'replaced_by', type: 'channel', value: replaced_by }, { name: 'replaced_by', type: 'channel', value: replaced_by },
{ name: 'website', type: 'external_link', value: data.website } { name: 'website', type: 'external_link', value: data.website }
].filter(f => (Array.isArray(f.value) ? f.value.length : f.value)) ].filter(f => (Array.isArray(f.value) ? f.value.length : f.value))
function norm(value) { function norm(value) {
return value.includes(' ') ? `"${value}"` : value return value.includes(' ') ? `"${value}"` : value
} }
function searchBy(q) { function searchBy(q) {
if ($query !== q) { if ($query !== q) {
query.set(q) query.set(q)
hasQuery.set(true) hasQuery.set(true)
search(q) setTimeout(() => {
setSearchParam('q', q) goto('/')
} }, 0)
close() }
} close()
}
</script> </script>
<div class="pb-8 px-8 pt-6 dark:text-white"> <table class="table-fixed w-full">
<div class="flex p-4 w-full"> <tbody>
<table class="table-fixed w-full"> {#each fieldset as field}
<tbody> <tr>
{#each fieldset as field} <td class="align-top w-[11rem]">
<tr> <div class="flex pr-4 py-1 text-sm text-gray-400 whitespace-nowrap dark:text-gray-400">
<td class="align-top w-[11rem]"> {field.name}
<div class="flex px-4 py-1 text-sm text-gray-400 whitespace-nowrap dark:text-gray-400"> </div>
{field.name} </td>
</div> <td class="align-top">
</td> <div class="flex py-1 text-sm text-gray-700 dark:text-gray-100 flex-wrap">
<td class="align-top"> {#if field.type === 'image'}
<div class="flex px-4 py-1 text-sm text-gray-700 dark:text-gray-100 flex-wrap"> <img
{#if field.type === 'image'} src="{field.value}"
<img alt="{field.name}"
src="{field.value}" loading="lazy"
alt="{field.name}" referrerpolicy="no-referrer"
loading="lazy" class="border rounded-sm overflow-hidden border-gray-200 bg-[#e6e6e6]"
referrerpolicy="no-referrer" />
class="border rounded-sm overflow-hidden border-gray-200 bg-[#e6e6e6]" {:else if field.type === 'link'}
/> <button
{:else if field.type === 'link'} on:click="{() => searchBy(field.value.query)}"
<button class="underline hover:text-blue-500"
on:click="{() => searchBy(field.value.query)}" >
class="underline hover:text-blue-500" {field.value.label}
> </button>
{field.value.label} {:else if field.type === 'link[]'} {#each field.value as value, i} {#if i > 0}<span
</button> >,&nbsp;
{:else if field.type === 'link[]'} {#each field.value as value, i} {#if i > 0}<span </span>
>,&nbsp; {/if}
</span> <button on:click="{() => searchBy(value.query)}" class="underline hover:text-blue-500">
{/if} {value.label}
<button </button>
on:click="{() => searchBy(value.query)}" {/each} {:else if field.type === 'external_link'}
class="underline hover:text-blue-500" <a
> href="{field.value}"
{value.label} class="underline hover:text-blue-500 inline-flex align-middle"
</button> target="_blank"
{/each} {:else if field.type === 'external_link'} rel="noopener noreferrer"
<a >{field.value}<span
href="{field.value}" class="inline-flex items-center pl-1 text-sm font-semibold text-gray-400 rounded-full"
class="underline hover:text-blue-500 inline-flex align-middle" >
target="_blank" <svg
rel="noopener noreferrer" class="w-4 h-4"
>{field.value}<span fill="none"
class="inline-flex items-center pl-1 text-sm font-semibold text-gray-400 rounded-full" stroke="currentColor"
> viewBox="0 0 24 24"
<svg xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4" >
fill="none" <path
stroke="currentColor" stroke-linecap="round"
viewBox="0 0 24 24" stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg" stroke-width="2"
> d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
<path ></path>
stroke-linecap="round" </svg> </span
stroke-linejoin="round" ></a>
stroke-width="2" {:else} {field.value} {/if}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" </div>
></path> </td>
</svg> </span </tr>
></a> {/each}
{:else} {field.value} {/if} </tbody>
</div> </table>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>

View file

@ -1,9 +1,9 @@
<script> <script>
import { query, search, setSearchParam } from '../store.js' import { query, search, setSearchParam } from '~/store'
import { goto } from '$app/navigation'
function onSubmit() { function onSubmit() {
setSearchParam('q', $query) goto('/')
search($query)
} }
</script> </script>

View file

@ -1,12 +1,7 @@
<script> <script>
import '~/app.css' import '~/app.css'
import NavBar from '~/components/NavBar.svelte'
import Modal from 'svelte-simple-modal'
let scrollTop = 0
</script> </script>
<svelte:window bind:scrollY="{scrollTop}" />
<svelte:head> <svelte:head>
<script> <script>
if (document) { if (document) {
@ -22,20 +17,4 @@
</script> </script>
</svelte:head> </svelte:head>
<header <slot />
class:absolute="{scrollTop <= 150}"
class:fixed="{scrollTop > 150}"
class="z-40 w-full min-w-[360px]"
style="top: {scrollTop > 150 && scrollTop <= 210 ? scrollTop-210: 0}px"
>
<NavBar withSearch="{scrollTop > 150}" />
</header>
<main class="bg-slate-50 dark:bg-[#1d232e] min-h-screen pt-10 min-w-[360px]">
<Modal
unstyled="{true}"
classBg="fixed top-0 left-0 z-40 w-screen h-screen flex flex-col bg-black/[.7] overflow-y-scroll"
closeButton="{false}"
><slot
/></Modal>
</main>

View file

@ -1,7 +1,11 @@
<script> <script>
import NavBar from '~/components/NavBar.svelte'
import Modal from 'svelte-simple-modal'
import { page } from '$app/stores'
import InfiniteLoading from 'svelte-infinite-loading' import InfiniteLoading from 'svelte-infinite-loading'
import { import {
fetchChannels, fetchChannels,
channels,
hasQuery, hasQuery,
countries, countries,
filteredChannels, filteredChannels,
@ -14,6 +18,7 @@
import CountryItem from '~/components/CountryItem.svelte' import CountryItem from '~/components/CountryItem.svelte'
import SearchField from '~/components/SearchField.svelte' import SearchField from '~/components/SearchField.svelte'
import _ from 'lodash' import _ from 'lodash'
import { afterNavigate } from '$app/navigation'
let _countries = [] let _countries = []
const initLimit = 10 const initLimit = 10
@ -52,7 +57,9 @@
hasQuery.set(true) hasQuery.set(true)
} }
await fetchChannels() if (!$channels.length) {
await fetchChannels()
}
_countries = Object.values($countries) _countries = Object.values($countries)
isLoading = false isLoading = false
@ -73,34 +80,62 @@
} }
} }
}) })
afterNavigate(() => {
setSearchParam('q', $query)
search($query)
})
let scrollTop = 0
</script> </script>
<svelte:window bind:scrollY="{scrollTop}" />
<svelte:head> <svelte:head>
<title>iptv-org</title> <title>iptv-org</title>
<meta name="description" content="Collection of resources dedicated to IPTV" /> <meta name="description" content="Collection of resources dedicated to IPTV" />
</svelte:head> </svelte:head>
<section class="container max-w-5xl mx-auto px-2 py-20"> <header
<SearchField bind:isLoading="{isLoading}" bind:found="{$filteredChannels.length}"></SearchField> class:absolute="{scrollTop <= 150}"
{#if isLoading} class:fixed="{scrollTop > 150}"
<div class="z-40 w-full min-w-[360px]"
class="flex items-center justify-center w-full pt-1 pb-6 tracking-tight text-sm text-gray-500 dark:text-gray-400 font-mono" style="top: {scrollTop > 150 && scrollTop <= 210 ? scrollTop-210: 0}px"
>
<NavBar withSearch="{scrollTop > 150}" />
</header>
<main class="bg-slate-50 dark:bg-[#1d232e] min-h-screen pt-10 min-w-[360px]">
<Modal
unstyled="{true}"
classBg="fixed top-0 left-0 z-40 w-screen h-screen flex flex-col bg-black/[.7] overflow-y-scroll"
closeButton="{false}"
> >
loading... <section class="container max-w-5xl mx-auto px-2 py-20">
</div> <SearchField
{/if} {#each visible as country (country.code)} {#if grouped[country.code] && bind:isLoading="{isLoading}"
grouped[country.code].length > 0} bind:found="{$filteredChannels.length}"
<CountryItem ></SearchField>
bind:country="{country}" {#if isLoading}
bind:channels="{grouped[country.code]}" <div
bind:hasQuery="{$hasQuery}" class="flex items-center justify-center w-full pt-1 pb-6 tracking-tight text-sm text-gray-500 dark:text-gray-400 font-mono"
></CountryItem> >
{/if} {/each} {#if !isLoading} loading...
<InfiniteLoading on:infinite="{loadMore}" identifier="{infiniteId}" distance="{500}"> </div>
<div slot="noResults"></div> {/if} {#each visible as country (country.code)} {#if grouped[country.code] &&
<div slot="noMore"></div> grouped[country.code].length > 0}
<div slot="error"></div> <CountryItem
<div slot="spinner"></div> bind:country="{country}"
</InfiniteLoading> bind:channels="{grouped[country.code]}"
{/if} bind:hasQuery="{$hasQuery}"
</section> ></CountryItem>
{/if} {/each} {#if !isLoading}
<InfiniteLoading on:infinite="{loadMore}" identifier="{infiniteId}" distance="{500}">
<div slot="noResults"></div>
<div slot="noMore"></div>
<div slot="error"></div>
<div slot="spinner"></div>
</InfiniteLoading>
{/if}
</section>
</Modal>
</main>

View file

@ -0,0 +1,94 @@
<script>
import GuideItem from '~/components/GuideItem.svelte'
import StreamItem from '~/components/StreamItem.svelte'
import HTMLPreview from '~/components/HTMLPreview.svelte'
import NavBar from '~/components/NavBar.svelte'
import { onMount } from 'svelte'
import { fetchChannels, channels } from '~/store'
import { page } from '$app/stores'
let channel
let isLoading = true
let streams = []
let guides = []
onMount(async () => {
const id = $page.url.searchParams.get('id')
if (id && !$channels.length) {
await fetchChannels()
}
channel = $channels.find(c => c.id === id)
if (channel) {
streams = channel._streams
guides = channel._guides
}
isLoading = false
})
</script>
<svelte:head>
<title>{channel && channel.name ? `${channel.name} iptv-org` : 'iptv-org'}</title>
</svelte:head>
<header class="fixed z-40 w-full min-w-[360px] top-0">
<NavBar withSearch />
</header>
<main class="bg-slate-50 dark:bg-[#1d232e] min-h-screen min-w-[360px] pt-16">
<section class="container max-w-[820px] mx-auto px-2 pt-6 pb-20 flex-col space-y-4">
{#if isLoading}
<div
class="flex items-center justify-center w-full pt-1 pb-6 tracking-tight text-sm text-gray-500 dark:text-gray-400 font-mono"
>
loading...
</div>
{/if} {#if channel}
<div class="border rounded-md border-gray-200 dark:border-gray-700 dark:bg-gray-800 bg-white">
<div
class="flex justify-between items-center py-4 pl-5 pr-4 rounded-t border-b dark:border-gray-700"
>
<div class="w-1/3 overflow-hidden">
<h1 class="text-l font-medium text-gray-900 dark:text-white">{channel.name}</h1>
</div>
</div>
<div class="overflow-y-scroll overflow-x-hidden w-full p-10">
<HTMLPreview data="{channel}" />
</div>
</div>
{/if} {#if streams.length}
<div class="border rounded-md border-gray-200 dark:border-gray-700 dark:bg-gray-800 bg-white">
<div
class="flex justify-between items-center py-4 pl-5 pr-4 rounded-t border-b dark:border-gray-700"
>
<div class="w-1/3 overflow-hidden">
<h3 class="text-l font-medium text-gray-900 dark:text-white">Streams</h3>
</div>
</div>
<div class="overflow-y-scroll overflow-x-hidden w-full p-6">
<div class="space-y-2">
{#each streams as stream}
<StreamItem stream="{stream}" />
{/each}
</div>
</div>
</div>
{/if} {#if guides.length}
<div class="border rounded-md border-gray-200 dark:border-gray-700 dark:bg-gray-800 bg-white">
<div
class="flex justify-between items-center py-4 pl-5 pr-4 rounded-t border-b dark:border-gray-700"
>
<div class="w-1/3 overflow-hidden">
<h3 class="text-l font-medium text-gray-900 dark:text-white">Guides</h3>
</div>
</div>
<div class="overflow-y-scroll overflow-x-hidden w-full p-6">
<div class="space-y-2">
{#each guides as guide}
<GuideItem guide="{guide}" />
{/each}
</div>
</div>
</div>
{/if}
</section>
</main>