mirror of
https://github.com/iptv-org/iptv-org.github.io.git
synced 2025-05-11 17:40:05 -04:00
Creates /channel page
This commit is contained in:
parent
677365c6da
commit
33d6a13667
6 changed files with 456 additions and 347 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
>,
|
||||||
{:else if field.type === 'link[]'} {#each field.value as value, i} {#if i > 0}<span
|
</span>
|
||||||
>,
|
{/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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
94
src/pages/channel/+page.svelte
Normal file
94
src/pages/channel/+page.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue