mirror of
https://github.com/iptv-org/iptv-org.github.io.git
synced 2025-05-11 17:40:05 -04:00
Merge d52ec0f695
into fcb707b52f
This commit is contained in:
commit
b9a3189234
41 changed files with 789 additions and 561 deletions
19
package-lock.json
generated
19
package-lock.json
generated
|
@ -10,7 +10,7 @@
|
||||||
"@freearhey/core": "^0.8.2",
|
"@freearhey/core": "^0.8.2",
|
||||||
"@freearhey/search-js": "^0.1.2",
|
"@freearhey/search-js": "^0.1.2",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.20.4",
|
"@sveltejs/kit": "^2.20.7",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
"@types/qs": "^6.9.18",
|
"@types/qs": "^6.9.18",
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
|
"run-script-os": "^1.1.6",
|
||||||
"svelte": "^5.22.6",
|
"svelte": "^5.22.6",
|
||||||
"svelte-simple-modal": "^2.0.0",
|
"svelte-simple-modal": "^2.0.0",
|
||||||
"svelte-sitemap": "^2.6.0",
|
"svelte-sitemap": "^2.6.0",
|
||||||
|
@ -947,9 +948,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
"version": "2.20.4",
|
"version": "2.20.7",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.4.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz",
|
||||||
"integrity": "sha512-B3Y1mb1Qjt57zXLVch5tfqsK/ebHe6uYTcFSnGFNwRpId3+fplLgQK6Z2zhDVBezSsPuhDq6Pry+9PA88ocN6Q==",
|
"integrity": "sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
|
@ -3033,6 +3034,16 @@
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/run-script-os": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"run-os": "index.js",
|
||||||
|
"run-script-os": "index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
"act:update": "act workflow_dispatch -W .github/workflows/update.yml",
|
"act:update": "act workflow_dispatch -W .github/workflows/update.yml",
|
||||||
"api:load": "tsx ./src/commands/api/load.ts",
|
"api:load": "tsx ./src/commands/api/load.ts",
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vite build",
|
"build": "run-script-os",
|
||||||
|
"build:win32": "SET \"NODE_OPTIONS=--max_old_space_size=4096\" && vite build",
|
||||||
|
"build:default": "NODE_OPTIONS=--max_old_space_size=4096 vite build",
|
||||||
"preview": "vite preview --host",
|
"preview": "vite preview --host",
|
||||||
"prepare": "svelte-kit sync",
|
"prepare": "svelte-kit sync",
|
||||||
"postinstall": "npm run api:load",
|
"postinstall": "npm run api:load",
|
||||||
|
@ -17,7 +19,7 @@
|
||||||
"@freearhey/core": "^0.8.2",
|
"@freearhey/core": "^0.8.2",
|
||||||
"@freearhey/search-js": "^0.1.2",
|
"@freearhey/search-js": "^0.1.2",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.20.4",
|
"@sveltejs/kit": "^2.20.7",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
"@types/qs": "^6.9.18",
|
"@types/qs": "^6.9.18",
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
|
"run-script-os": "^1.1.6",
|
||||||
"svelte": "^5.22.6",
|
"svelte": "^5.22.6",
|
||||||
"svelte-simple-modal": "^2.0.0",
|
"svelte-simple-modal": "^2.0.0",
|
||||||
"svelte-sitemap": "^2.6.0",
|
"svelte-sitemap": "^2.6.0",
|
||||||
|
|
|
@ -13,14 +13,14 @@
|
||||||
{$selected.count()} selected
|
{$selected.count()} selected
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-1 sm:space-x-2 items-center">
|
<div class="flex space-x-1 sm:space-x-2 items-center">
|
||||||
<ResetButton />
|
<ResetButton variant="dark" />
|
||||||
<SelectAllButton />
|
<SelectAllButton variant="dark" />
|
||||||
<DownloadButton />
|
<DownloadButton variant="dark" />
|
||||||
<CloseButton
|
<CloseButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
downloadMode.set(false)
|
downloadMode.set(false)
|
||||||
}}
|
}}
|
||||||
variant="light"
|
variant="dark"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
class="w-full text-left rounded-md text-sm h-10 flex items-center text-gray-500 dark:text-gray-400 font-normal hover:bg-gray-100 dark:hover:bg-primary-750 space-x-3 px-2 border border-transparent cursor-pointer"
|
class="w-full text-left rounded-md text-sm h-10 flex items-center text-gray-500 dark:text-gray-400 font-normal hover:bg-gray-100 dark:hover:bg-primary-750 space-x-3 px-2 border border-transparent cursor-pointer"
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<div class="w-5 h-5 flex items-center justify-center">
|
<div class="w-5 flex shrink-0 items-center justify-center">
|
||||||
<slot name="left" />
|
<slot name="left" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">{label}</div>
|
<div class="w-full">{label}</div>
|
||||||
<div>
|
<div class="w-4 flex shrink-0">
|
||||||
<slot name="right" />
|
<slot name="right" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
23
src/components/ChannelMenu.svelte
Normal file
23
src/components/ChannelMenu.svelte
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ChannelRemoveButton, ChannelEditButton, CopyLinkButton, Menu } from '~/components'
|
||||||
|
import { toast } from '@zerodevx/svelte-toast'
|
||||||
|
import type { Channel } from '~/models'
|
||||||
|
|
||||||
|
export let channel: Channel
|
||||||
|
|
||||||
|
let isMenuOpened = false
|
||||||
|
function closeMenu() {
|
||||||
|
isMenuOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLinkCopy() {
|
||||||
|
toast.push('Link copied to clipboard')
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu bind:isOpened={isMenuOpened}>
|
||||||
|
<CopyLinkButton link={channel.getPageUrl()} onCopy={onLinkCopy} />
|
||||||
|
<ChannelEditButton {channel} onClick={closeMenu} />
|
||||||
|
<ChannelRemoveButton {channel} onClick={closeMenu} />
|
||||||
|
</Menu>
|
|
@ -1,20 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Context } from 'svelte-simple-modal'
|
import type { Context } from 'svelte-simple-modal'
|
||||||
import { toast } from '@zerodevx/svelte-toast'
|
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import { Channel } from '~/models'
|
import { Channel } from '~/models'
|
||||||
import {
|
import {
|
||||||
ChannelRemoveButton,
|
|
||||||
ShareChannelButton,
|
ShareChannelButton,
|
||||||
ChannelEditButton,
|
|
||||||
CopyLinkButton,
|
|
||||||
BlockedBadge,
|
BlockedBadge,
|
||||||
CloseButton,
|
CloseButton,
|
||||||
ClosedBadge,
|
ClosedBadge,
|
||||||
|
ChannelMenu,
|
||||||
HTMLPreview,
|
HTMLPreview,
|
||||||
Popup,
|
Popup,
|
||||||
Card,
|
Card
|
||||||
Menu
|
|
||||||
} from '~/components'
|
} from '~/components'
|
||||||
|
|
||||||
export let channel: Channel
|
export let channel: Channel
|
||||||
|
@ -29,16 +25,6 @@
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isMenuOpened = false
|
|
||||||
function closeMenu() {
|
|
||||||
isMenuOpened = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLinkCopy() {
|
|
||||||
toast.push('Link copied to clipboard')
|
|
||||||
closeMenu()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popup onClose={close}>
|
<Popup onClose={close}>
|
||||||
|
@ -58,11 +44,7 @@
|
||||||
{#if isTouchDevice}
|
{#if isTouchDevice}
|
||||||
<ShareChannelButton {channel} />
|
<ShareChannelButton {channel} />
|
||||||
{/if}
|
{/if}
|
||||||
<Menu bind:isOpened={isMenuOpened}>
|
<ChannelMenu {channel} />
|
||||||
<CopyLinkButton link={channel.getPageUrl()} onCopy={onLinkCopy} />
|
|
||||||
<ChannelEditButton {channel} onClick={closeMenu} />
|
|
||||||
<ChannelRemoveButton {channel} onClick={closeMenu} />
|
|
||||||
</Menu>
|
|
||||||
<CloseButton onClick={close} />
|
<CloseButton onClick={close} />
|
||||||
</div>
|
</div>
|
||||||
<div slot="body" class="pt-4 pb-3 px-4 sm:py-9 sm:px-11">
|
<div slot="body" class="pt-4 pb-3 px-4 sm:py-9 sm:px-11">
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
const params = qs.stringify({
|
const params = qs.stringify({
|
||||||
labels: 'channels:remove',
|
labels: 'channels:remove',
|
||||||
template: '3_channels_remove.yml',
|
template: '3_channels_remove.yml',
|
||||||
title: `Edit: ${channel.getUniqueName()}`,
|
title: `Remove: ${channel.getUniqueName()}`,
|
||||||
id: channel.id
|
id: channel.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import { selected } from '~/store'
|
import { selected } from '~/store'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
|
export let variant = 'default'
|
||||||
|
|
||||||
const playlistCreator = new PlaylistCreator()
|
const playlistCreator = new PlaylistCreator()
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
disabled={!$selected.count()}
|
disabled={!$selected.count()}
|
||||||
aria-label="Download Playlist"
|
aria-label="Download Playlist"
|
||||||
title="Download Playlist"
|
title="Download Playlist"
|
||||||
variant="light"
|
{variant}
|
||||||
>
|
>
|
||||||
<Icon.Download size={16} />
|
<Icon.Download size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
const params = qs.stringify({
|
const params = qs.stringify({
|
||||||
labels: 'feeds:add',
|
labels: 'feeds:add',
|
||||||
template: '4_feeds_add.yml',
|
template: '4_feeds_add.yml',
|
||||||
title: 'Add: ',
|
title: `Add: ${channel.name} Feed`,
|
||||||
channel_id: channel.id
|
channel_id: channel.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button onClick={_onClick} label="Add feed">
|
<Button onClick={_onClick} label="Add Feed">
|
||||||
<Icon.Add slot="left" class="text-gray-400" size={20} />
|
<Icon.Add slot="left" class="text-gray-400" size={19} />
|
||||||
<Icon.ExternalLink slot="right" class="text-gray-400 dark:text-gray-500" size={17} />
|
<Icon.ExternalLink slot="right" class="text-gray-400 dark:text-gray-500" size={17} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
28
src/components/FeedAddIconButton.svelte
Normal file
28
src/components/FeedAddIconButton.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import IconButton from '~/components/IconButton.svelte'
|
||||||
|
import type { Channel } from '~/models'
|
||||||
|
import * as Icon from '~/icons'
|
||||||
|
import qs from 'qs'
|
||||||
|
|
||||||
|
export let channel: Channel
|
||||||
|
export let onClick = () => {}
|
||||||
|
|
||||||
|
const endpoint = 'https://github.com/iptv-org/database/issues/new'
|
||||||
|
const params = qs.stringify({
|
||||||
|
labels: 'feeds:add',
|
||||||
|
template: '4_feeds_add.yml',
|
||||||
|
title: `Add: ${channel.name} Feed`,
|
||||||
|
channel_id: channel.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `${endpoint}?${params}`
|
||||||
|
|
||||||
|
function _onClick() {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IconButton onClick={_onClick} title="Add Feed">
|
||||||
|
<Icon.AddCircle class="text-gray-400" size={20} />
|
||||||
|
</IconButton>
|
|
@ -1,20 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Context } from 'svelte-simple-modal'
|
import type { Context } from 'svelte-simple-modal'
|
||||||
import { toast } from '@zerodevx/svelte-toast'
|
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import { page } from '$app/state'
|
import { page } from '$app/state'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
import { Feed } from '~/models'
|
import { Feed } from '~/models'
|
||||||
import {
|
import {
|
||||||
FeedRemoveButton,
|
|
||||||
CopyLinkButton,
|
|
||||||
FeedEditButton,
|
|
||||||
ExpandButton,
|
ExpandButton,
|
||||||
StreamsPopup,
|
StreamsPopup,
|
||||||
HTMLPreview,
|
HTMLPreview,
|
||||||
GuidesPopup,
|
GuidesPopup,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
Menu
|
FeedMenu
|
||||||
} from '~/components'
|
} from '~/components'
|
||||||
|
|
||||||
export let feed: Feed
|
export let feed: Feed
|
||||||
|
@ -28,7 +24,7 @@
|
||||||
function showGuides() {
|
function showGuides() {
|
||||||
modal.open(
|
modal.open(
|
||||||
GuidesPopup,
|
GuidesPopup,
|
||||||
{ guides: feed.getGuides(), title: 'Guides' },
|
{ feed },
|
||||||
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +32,7 @@
|
||||||
function showStreams() {
|
function showStreams() {
|
||||||
modal.open(
|
modal.open(
|
||||||
StreamsPopup,
|
StreamsPopup,
|
||||||
{ streams: feed.getStreams(), title: 'Streams' },
|
{ feed },
|
||||||
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,16 +41,6 @@
|
||||||
modal.close()
|
modal.close()
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
let isMenuOpened = false
|
|
||||||
function closeMenu() {
|
|
||||||
isMenuOpened = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLinkCopy() {
|
|
||||||
toast.push('Link copied to clipboard')
|
|
||||||
closeMenu()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full rounded-md border border-gray-200 dark:border-gray-700" id={feed.id}>
|
<div class="w-full rounded-md border border-gray-200 dark:border-gray-700" id={feed.id}>
|
||||||
|
@ -86,23 +72,19 @@
|
||||||
<button
|
<button
|
||||||
onclick={showGuides}
|
onclick={showGuides}
|
||||||
class="text-sm text-gray-400 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer"
|
class="text-sm text-gray-400 inline-flex space-x-1 flex items-center hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer"
|
||||||
title="Streams"
|
title="Guides"
|
||||||
>
|
>
|
||||||
<Icon.Guide size={20} />
|
<Icon.Guide size={20} />
|
||||||
<div>{feed.getGuides().count()}</div>
|
<div>{feed.getGuides().count()}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Menu bind:isOpened={isMenuOpened}>
|
<FeedMenu {feed} />
|
||||||
<CopyLinkButton link={feed.getPageUrl()} onCopy={onLinkCopy} />
|
|
||||||
<FeedEditButton {feed} onClick={closeMenu} />
|
|
||||||
<FeedRemoveButton {feed} onClick={closeMenu} />
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<div class="w-full flex px-6 py-6">
|
<div class="w-full flex px-6 pt-5 pb-2">
|
||||||
<HTMLPreview fieldset={feed.getFieldset()} onClick={_onClose} />
|
<HTMLPreview fieldset={feed.getFieldset()} onClick={_onClose} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
30
src/components/FeedMenu.svelte
Normal file
30
src/components/FeedMenu.svelte
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from '@zerodevx/svelte-toast'
|
||||||
|
import type { Feed } from '~/models'
|
||||||
|
import {
|
||||||
|
FeedRemoveButton,
|
||||||
|
StreamAddButton,
|
||||||
|
CopyLinkButton,
|
||||||
|
FeedEditButton,
|
||||||
|
Menu
|
||||||
|
} from '~/components'
|
||||||
|
|
||||||
|
export let feed: Feed
|
||||||
|
|
||||||
|
let isMenuOpened = false
|
||||||
|
function closeMenu() {
|
||||||
|
isMenuOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLinkCopy() {
|
||||||
|
toast.push('Link copied to clipboard')
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu bind:isOpened={isMenuOpened}>
|
||||||
|
<CopyLinkButton link={feed.getPageUrl()} onCopy={onLinkCopy} />
|
||||||
|
<StreamAddButton {feed} onClick={closeMenu} />
|
||||||
|
<FeedEditButton {feed} onClick={closeMenu} />
|
||||||
|
<FeedRemoveButton {feed} onClick={closeMenu} />
|
||||||
|
</Menu>
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Popup, Card, Menu, FeedAddButton, CloseButton } from '~/components'
|
import { Popup, Card, FeedAddIconButton, CloseButton } from '~/components'
|
||||||
import { Collection } from '@freearhey/core/browser'
|
import { Collection } from '@freearhey/core/browser'
|
||||||
import type { Context } from 'svelte-simple-modal'
|
import type { Context } from 'svelte-simple-modal'
|
||||||
import type { Channel, Feed } from '~/models'
|
import type { Channel, Feed } from '~/models'
|
||||||
|
@ -17,11 +17,6 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
const { close } = getContext<Context>('simple-modal')
|
const { close } = getContext<Context>('simple-modal')
|
||||||
|
|
||||||
let isMenuOpened = false
|
|
||||||
function closeMenu() {
|
|
||||||
isMenuOpened = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popup onClose={close}>
|
<Popup onClose={close}>
|
||||||
|
@ -37,9 +32,7 @@
|
||||||
</span>{channel.getDisplayName()}
|
</span>{channel.getDisplayName()}
|
||||||
</div>
|
</div>
|
||||||
<div slot="headerRight" class="inline-flex">
|
<div slot="headerRight" class="inline-flex">
|
||||||
<Menu bind:isOpened={isMenuOpened}>
|
<FeedAddIconButton {channel} />
|
||||||
<FeedAddButton {channel} onClick={closeMenu} />
|
|
||||||
</Menu>
|
|
||||||
<CloseButton onClick={close} />
|
<CloseButton onClick={close} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
const params = qs.stringify({
|
const params = qs.stringify({
|
||||||
labels: 'feeds:remove',
|
labels: 'feeds:remove',
|
||||||
template: '6_feeds_remove.yml',
|
template: '6_feeds_remove.yml',
|
||||||
title: `Edit: ${feed.getDisplayName()}`,
|
title: `Remove: ${feed.getDisplayName()}`,
|
||||||
feed_id: feed.id,
|
feed_id: feed.id,
|
||||||
channel_id: feed.channelId
|
channel_id: feed.channelId
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CloseButton, GuideItem, Popup, Card } from '~/components'
|
import { CloseButton, GuideItem, Popup, Card } from '~/components'
|
||||||
import { Collection } from '@freearhey/core/browser'
|
|
||||||
import type { Context } from 'svelte-simple-modal'
|
import type { Context } from 'svelte-simple-modal'
|
||||||
|
import type { Feed } from '~/models'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
|
export let feed: Feed
|
||||||
export let title = 'Guides'
|
export let title = 'Guides'
|
||||||
export let guides: Collection = new Collection()
|
|
||||||
|
|
||||||
const { close } = getContext<Context>('simple-modal')
|
const { close } = getContext<Context>('simple-modal')
|
||||||
</script>
|
</script>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div slot="body" class="p-2 sm:p-5 w-full">
|
<div slot="body" class="p-2 sm:p-5 w-full">
|
||||||
<div class="dark:border-gray-700 rounded-md border border-gray-200">
|
<div class="dark:border-gray-700 rounded-md border border-gray-200">
|
||||||
{#each guides.all() as guide}
|
{#each feed.getGuides().all() as guide}
|
||||||
<GuideItem {guide} />
|
<GuideItem {guide} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,74 +8,76 @@
|
||||||
<table class="table-fixed w-full">
|
<table class="table-fixed w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each fieldset as field}
|
{#each fieldset as field}
|
||||||
<tr>
|
{#if field}
|
||||||
<td class="align-top w-[140px] sm:w-[200px]">
|
<tr>
|
||||||
<div class="flex pr-5 pb-3 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400">
|
<td class="align-top w-[135px] sm:w-[200px]">
|
||||||
{field.name}
|
<div class="flex pr-5 pb-3 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||||
</div>
|
{field.name}
|
||||||
</td>
|
</div>
|
||||||
<td class="align-top w-full overflow-hidden">
|
</td>
|
||||||
<div class="pb-3 text-sm text-gray-900 dark:text-gray-100">
|
<td class="align-top w-full overflow-hidden">
|
||||||
{#if field.type === 'image'}
|
<div class="pb-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
<img
|
{#if field.type === 'image'}
|
||||||
src={field.value.src}
|
<img
|
||||||
alt={field.value.alt}
|
src={field.value.src}
|
||||||
title={field.value.title}
|
alt={field.value.alt}
|
||||||
referrerpolicy="no-referrer"
|
title={field.value.title}
|
||||||
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'}
|
/>
|
||||||
<div class="truncate">
|
{:else if field.type === 'link'}
|
||||||
<a
|
<div class="truncate">
|
||||||
href="/?q={field.value.query}"
|
|
||||||
onclick={onClick}
|
|
||||||
class="underline hover:text-blue-400"
|
|
||||||
title={field.value.label}
|
|
||||||
>
|
|
||||||
{field.value.label}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{:else if field.type === 'link[]'}
|
|
||||||
<div class="overflow-hidden text-ellipsis">
|
|
||||||
{#each field.value as value, i}
|
|
||||||
{#if i > 0}<span>, </span>
|
|
||||||
{/if}
|
|
||||||
<a
|
<a
|
||||||
href="/?q={value.query}"
|
href="/?q={field.value.query}"
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
class="underline hover:text-blue-400"
|
class="underline hover:text-blue-400"
|
||||||
title={value.label}
|
title={field.value.label}
|
||||||
>
|
>
|
||||||
{value.label}
|
{field.value.label}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{:else if field.type === 'link[]'}
|
||||||
{:else if field.type === 'external_link'}
|
<div class="overflow-hidden text-ellipsis">
|
||||||
<div class="truncate">
|
{#each field.value as value, i}
|
||||||
<a
|
{#if i > 0}<span>, </span>
|
||||||
href={field.value.href}
|
{/if}
|
||||||
class="underline hover:text-blue-400"
|
<a
|
||||||
target="_blank"
|
href="/?q={value.query}"
|
||||||
rel="noopener noreferrer"
|
onclick={onClick}
|
||||||
title={field.value.title}>{field.value.label}</a
|
class="underline hover:text-blue-400"
|
||||||
>
|
title={value.label}
|
||||||
</div>
|
>
|
||||||
{:else if field.name === 'id'}
|
{value.label}
|
||||||
<span class="break-all" title={field.value.toString()}>{field.value}</span>
|
</a>
|
||||||
{:else if field.type === 'string[]'}
|
{/each}
|
||||||
<div class="overflow-hidden text-ellipsis">
|
</div>
|
||||||
{#each field.value as value, i}
|
{:else if field.type === 'external_link'}
|
||||||
{#if i > 0}<span>, </span>
|
<div class="truncate">
|
||||||
{/if}
|
<a
|
||||||
<span title={value.toString()}>{value}</span>
|
href={field.value.href}
|
||||||
{/each}
|
class="underline hover:text-blue-400"
|
||||||
</div>
|
target="_blank"
|
||||||
{:else if field.type === 'string'}
|
rel="noopener noreferrer"
|
||||||
<span title={field.title}>{field.value}</span>
|
title={field.value.title}>{field.value.label}</a
|
||||||
{/if}
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
{:else if field.name === 'id'}
|
||||||
</tr>
|
<span class="break-all" title={field.value.toString()}>{field.value}</span>
|
||||||
|
{:else if field.type === 'string[]'}
|
||||||
|
<div class="overflow-hidden text-ellipsis">
|
||||||
|
{#each field.value as value, i}
|
||||||
|
{#if i > 0}<span>, </span>
|
||||||
|
{/if}
|
||||||
|
<span title={value.toString()}>{value}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if field.type === 'string'}
|
||||||
|
<span class="break-words" title={field.title}>{field.value}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
export let variant = 'default'
|
export let variant = 'default'
|
||||||
export let size = 40
|
export let size = 40
|
||||||
|
|
||||||
let className = 'rounded-lg text-sm flex items-center justify-center cursor-pointer shrink-0'
|
let className =
|
||||||
if (variant === 'light') className += ' hover:bg-primary-810 text-gray-300'
|
'rounded-lg text-sm flex items-center justify-center cursor-pointer shrink-0 text-gray-400'
|
||||||
else className += ' hover:bg-gray-100 dark:hover:bg-primary-750 text-gray-400'
|
if (variant === 'dark') className += ' hover:bg-primary-750'
|
||||||
|
else if (variant === 'light') className += ' hover:bg-gray-100'
|
||||||
|
else className += ' hover:bg-gray-100 dark:hover:bg-primary-750'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -15,13 +15,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative" use:clickOutside on:outside={closeMenu}>
|
<div class="relative" use:clickOutside on:outside={closeMenu}>
|
||||||
<IconButton onClick={toggleMenu} aria-label="Menu">
|
<IconButton onClick={toggleMenu} aria-label="Menu" title="Menu">
|
||||||
<Icon.Menu size={16} />
|
<Icon.Menu size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{#if isOpened}
|
{#if isOpened}
|
||||||
<div
|
<div
|
||||||
class="rounded-md bg-white dark:bg-primary-810 absolute top-11 right-0 w-48 z-10 p-1 border border-gray-200 dark:border-primary-750"
|
class="rounded-md bg-white dark:bg-primary-810 absolute top-10 right-0 w-48 z-10 p-1 border border-gray-200 dark:border-primary-750"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
import { selected } from '~/store'
|
import { selected } from '~/store'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
|
export let variant = 'default'
|
||||||
|
|
||||||
let isAnySelected = true
|
let isAnySelected = true
|
||||||
|
|
||||||
selected.subscribe((_selected: Collection) => {
|
selected.subscribe((_selected: Collection) => {
|
||||||
|
@ -16,7 +18,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAnySelected}
|
{#if isAnySelected}
|
||||||
<IconButton onClick={reset} aria-label="Reset" title="Reset" variant="light">
|
<IconButton onClick={reset} aria-label="Reset" title="Reset" {variant}>
|
||||||
<Icon.Reset size={24} />
|
<Icon.Reset size={24} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
import { Channel } from '~/models'
|
import { Channel } from '~/models'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
|
export let variant = 'default'
|
||||||
|
|
||||||
const channelsWithStreams: Collection = $channels.filter((channel: Channel) =>
|
const channelsWithStreams: Collection = $channels.filter((channel: Channel) =>
|
||||||
channel.hasStreams()
|
channel.hasStreams()
|
||||||
)
|
)
|
||||||
|
@ -91,11 +93,11 @@
|
||||||
<Icon.Spinner size={21} />
|
<Icon.Spinner size={21} />
|
||||||
</div>
|
</div>
|
||||||
{:else if isAllSelected}
|
{:else if isAllSelected}
|
||||||
<IconButton onClick={deselectAll} aria-label="Deselect All" title="Deselect All" variant="light">
|
<IconButton onClick={deselectAll} aria-label="Deselect All" title="Deselect All" {variant}>
|
||||||
<Icon.DeselectAll size={24} />
|
<Icon.DeselectAll size={24} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{:else}
|
{:else}
|
||||||
<IconButton onClick={selectAll} aria-label="Select All" title="Select All" variant="light">
|
<IconButton onClick={selectAll} aria-label="Select All" title="Select All" {variant}>
|
||||||
<Icon.SelectAll size={24} />
|
<Icon.SelectAll size={24} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
29
src/components/StreamAddButton.svelte
Normal file
29
src/components/StreamAddButton.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '~/components/Button.svelte'
|
||||||
|
import type { Feed } from '~/models'
|
||||||
|
import * as Icon from '~/icons'
|
||||||
|
import qs from 'qs'
|
||||||
|
|
||||||
|
export let feed: Feed
|
||||||
|
export let onClick = () => {}
|
||||||
|
|
||||||
|
const endpoint = 'https://github.com/iptv-org/iptv/issues/new'
|
||||||
|
const params = qs.stringify({
|
||||||
|
labels: 'streams:add',
|
||||||
|
template: '1_streams_add.yml',
|
||||||
|
title: `Add: ${feed.getDisplayName()}`,
|
||||||
|
stream_id: feed.getStreamId()
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `${endpoint}?${params}`
|
||||||
|
|
||||||
|
function _onClick() {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button onClick={_onClick} label="Add Stream">
|
||||||
|
<Icon.Stream slot="left" class="text-gray-400" size={19} />
|
||||||
|
<Icon.ExternalLink slot="right" class="text-gray-400 dark:text-gray-500" size={17} />
|
||||||
|
</Button>
|
28
src/components/StreamAddIconButton.svelte
Normal file
28
src/components/StreamAddIconButton.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import IconButton from '~/components/IconButton.svelte'
|
||||||
|
import type { Feed } from '~/models'
|
||||||
|
import * as Icon from '~/icons'
|
||||||
|
import qs from 'qs'
|
||||||
|
|
||||||
|
export let feed: Feed
|
||||||
|
export let onClick = () => {}
|
||||||
|
|
||||||
|
const endpoint = 'https://github.com/iptv-org/iptv/issues/new'
|
||||||
|
const params = qs.stringify({
|
||||||
|
labels: 'streams:add',
|
||||||
|
template: '1_streams_add.yml',
|
||||||
|
title: `Add: ${feed.getDisplayName()}`,
|
||||||
|
stream_id: feed.getStreamId()
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `${endpoint}?${params}`
|
||||||
|
|
||||||
|
function _onClick() {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IconButton onClick={_onClick} title="Add Stream">
|
||||||
|
<Icon.AddCircle class="text-gray-400" size={20} />
|
||||||
|
</IconButton>
|
29
src/components/StreamEditButton.svelte
Normal file
29
src/components/StreamEditButton.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '~/components/Button.svelte'
|
||||||
|
import type { Stream } from '~/models'
|
||||||
|
import * as Icon from '~/icons'
|
||||||
|
import qs from 'qs'
|
||||||
|
|
||||||
|
export let stream: Stream
|
||||||
|
export let onClick = () => {}
|
||||||
|
|
||||||
|
const endpoint = 'https://github.com/iptv-org/iptv/issues/new'
|
||||||
|
const params = qs.stringify({
|
||||||
|
labels: 'streams:edit',
|
||||||
|
template: '2_streams_edit.yml',
|
||||||
|
title: `Edit: ${stream.getDisplayName()}`,
|
||||||
|
stream_url: stream.url
|
||||||
|
})
|
||||||
|
|
||||||
|
const editUrl = `${endpoint}?${params}`
|
||||||
|
|
||||||
|
function _onClick() {
|
||||||
|
window.open(editUrl, '_blank')
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button onClick={_onClick} label="Edit">
|
||||||
|
<Icon.Edit slot="left" class="text-gray-400" size={16} />
|
||||||
|
<Icon.ExternalLink slot="right" class="text-gray-400 dark:text-gray-500" size={17} />
|
||||||
|
</Button>
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CopyToClipboard, ExpandButton, JsonDataViewer } from '~/components'
|
import { StreamMenu, ExpandButton, HTMLPreview } from '~/components'
|
||||||
import { Stream } from '~/models'
|
import { Stream } from '~/models'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
|
@ -8,11 +8,9 @@
|
||||||
let isExpanded = false
|
let isExpanded = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="w-full rounded-md border border-gray-200 dark:border-gray-700">
|
||||||
class="w-full bg-gray-100 dark:bg-primary-750 dark:border-gray-600 rounded-md border border-gray-200"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="w-full inline-flex justify-between pl-2 pr-3 py-2 border-gray-200 dark:border-gray-600"
|
class="w-full inline-flex justify-between px-2 py-1.5 border-gray-200 dark:border-gray-700"
|
||||||
class:border-b={isExpanded}
|
class:border-b={isExpanded}
|
||||||
>
|
>
|
||||||
<div class="flex space-x-2 items-center w-full">
|
<div class="flex space-x-2 items-center w-full">
|
||||||
|
@ -33,14 +31,14 @@
|
||||||
<Icon.ExternalLink size={17} />
|
<Icon.ExternalLink size={17} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-8 justify-end shrink-0">
|
<div class="flex w-9 justify-end shrink-0">
|
||||||
<CopyToClipboard text={stream.url} />
|
<StreamMenu {stream} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<div class="w-full flex px-2 py-4">
|
<div class="w-full flex px-6 pt-5 pb-2">
|
||||||
<JsonDataViewer fieldset={stream.getFieldset()} />
|
<HTMLPreview fieldset={stream.getFieldset()} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
23
src/components/StreamMenu.svelte
Normal file
23
src/components/StreamMenu.svelte
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { CopyLinkButton, Menu, StreamEditButton, StreamReportButton } from '~/components'
|
||||||
|
import { toast } from '@zerodevx/svelte-toast'
|
||||||
|
import type { Stream } from '~/models'
|
||||||
|
|
||||||
|
export let stream: Stream
|
||||||
|
|
||||||
|
let isMenuOpened = false
|
||||||
|
function closeMenu() {
|
||||||
|
isMenuOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLinkCopy() {
|
||||||
|
toast.push('Link copied to clipboard')
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu bind:isOpened={isMenuOpened}>
|
||||||
|
<CopyLinkButton link={stream.url} onCopy={onLinkCopy} />
|
||||||
|
<StreamEditButton {stream} onClick={closeMenu} />
|
||||||
|
<StreamReportButton {stream} onClick={closeMenu} />
|
||||||
|
</Menu>
|
29
src/components/StreamReportButton.svelte
Normal file
29
src/components/StreamReportButton.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '~/components/Button.svelte'
|
||||||
|
import type { Stream } from '~/models'
|
||||||
|
import * as Icon from '~/icons'
|
||||||
|
import qs from 'qs'
|
||||||
|
|
||||||
|
export let stream: Stream
|
||||||
|
export let onClick = () => {}
|
||||||
|
|
||||||
|
const endpoint = 'https://github.com/iptv-org/iptv/issues/new'
|
||||||
|
const params = qs.stringify({
|
||||||
|
labels: 'streams:remove',
|
||||||
|
template: '3_streams_report.yml',
|
||||||
|
title: `Report: ${stream.getDisplayName()}`,
|
||||||
|
stream_url: stream.url
|
||||||
|
})
|
||||||
|
|
||||||
|
const editUrl = `${endpoint}?${params}`
|
||||||
|
|
||||||
|
function _onClick() {
|
||||||
|
window.open(editUrl, '_blank')
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button onClick={_onClick} label="Report">
|
||||||
|
<Icon.Alert slot="left" class="text-gray-400" size={17} />
|
||||||
|
<Icon.ExternalLink slot="right" class="text-gray-400 dark:text-gray-500" size={17} />
|
||||||
|
</Button>
|
|
@ -1,11 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CloseButton, StreamItem, Popup, Card } from '~/components'
|
import { CloseButton, StreamItem, Popup, Card, StreamAddIconButton } from '~/components'
|
||||||
import { Collection } from '@freearhey/core/browser'
|
|
||||||
import type { Context } from 'svelte-simple-modal'
|
import type { Context } from 'svelte-simple-modal'
|
||||||
|
import type { Feed } from '~/models'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import * as Icon from '~/icons'
|
import * as Icon from '~/icons'
|
||||||
|
|
||||||
export let streams: Collection = new Collection()
|
export let feed: Feed
|
||||||
export let title = 'Streams'
|
export let title = 'Streams'
|
||||||
|
|
||||||
const { close } = getContext<Context>('simple-modal')
|
const { close } = getContext<Context>('simple-modal')
|
||||||
|
@ -23,11 +23,12 @@
|
||||||
<Icon.Stream size={21} />
|
<Icon.Stream size={21} />
|
||||||
</span>{title}
|
</span>{title}
|
||||||
</div>
|
</div>
|
||||||
<div slot="headerRight">
|
<div slot="headerRight" class="inline-flex">
|
||||||
|
<StreamAddIconButton {feed} />
|
||||||
<CloseButton onClick={() => close()} />
|
<CloseButton onClick={() => close()} />
|
||||||
</div>
|
</div>
|
||||||
<div slot="body" class="flex flex-col gap-2 p-2 sm:p-5">
|
<div slot="body" class="flex flex-col gap-2 p-2 sm:p-5">
|
||||||
{#each streams.all() as stream, index (stream.getUUID())}
|
{#each feed.getStreams().all() as stream (stream.getUUID())}
|
||||||
<StreamItem {stream} />
|
<StreamItem {stream} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ export { default as Card } from './Card.svelte'
|
||||||
export { default as ChannelEditButton } from './ChannelEditButton.svelte'
|
export { default as ChannelEditButton } from './ChannelEditButton.svelte'
|
||||||
export { default as ChannelGrid } from './ChannelGrid.svelte'
|
export { default as ChannelGrid } from './ChannelGrid.svelte'
|
||||||
export { default as ChannelItem } from './ChannelItem.svelte'
|
export { default as ChannelItem } from './ChannelItem.svelte'
|
||||||
|
export { default as ChannelMenu } from './ChannelMenu.svelte'
|
||||||
export { default as ChannelPopup } from './ChannelPopup.svelte'
|
export { default as ChannelPopup } from './ChannelPopup.svelte'
|
||||||
export { default as ChannelRemoveButton } from './ChannelRemoveButton.svelte'
|
export { default as ChannelRemoveButton } from './ChannelRemoveButton.svelte'
|
||||||
export { default as Checkbox } from './Checkbox.svelte'
|
export { default as Checkbox } from './Checkbox.svelte'
|
||||||
|
@ -20,8 +21,10 @@ export { default as CreatePlaylistButton } from './CreatePlaylistButton.svelte'
|
||||||
export { default as DownloadButton } from './DownloadButton.svelte'
|
export { default as DownloadButton } from './DownloadButton.svelte'
|
||||||
export { default as ExpandButton } from './ExpandButton.svelte'
|
export { default as ExpandButton } from './ExpandButton.svelte'
|
||||||
export { default as FeedAddButton } from './FeedAddButton.svelte'
|
export { default as FeedAddButton } from './FeedAddButton.svelte'
|
||||||
|
export { default as FeedAddIconButton } from './FeedAddIconButton.svelte'
|
||||||
export { default as FeedEditButton } from './FeedEditButton.svelte'
|
export { default as FeedEditButton } from './FeedEditButton.svelte'
|
||||||
export { default as FeedItem } from './FeedItem.svelte'
|
export { default as FeedItem } from './FeedItem.svelte'
|
||||||
|
export { default as FeedMenu } from './FeedMenu.svelte'
|
||||||
export { default as FeedPopup } from './FeedPopup.svelte'
|
export { default as FeedPopup } from './FeedPopup.svelte'
|
||||||
export { default as FeedRemoveButton } from './FeedRemoveButton.svelte'
|
export { default as FeedRemoveButton } from './FeedRemoveButton.svelte'
|
||||||
export { default as GitHubButton } from './GitHubButton.svelte'
|
export { default as GitHubButton } from './GitHubButton.svelte'
|
||||||
|
@ -40,6 +43,11 @@ export { default as SearchField } from './SearchField.svelte'
|
||||||
export { default as SearchSyntaxPopup } from './SearchSyntaxPopup.svelte'
|
export { default as SearchSyntaxPopup } from './SearchSyntaxPopup.svelte'
|
||||||
export { default as SelectAllButton } from './SelectAllButton.svelte'
|
export { default as SelectAllButton } from './SelectAllButton.svelte'
|
||||||
export { default as ShareChannelButton } from './ShareChannelButton.svelte'
|
export { default as ShareChannelButton } from './ShareChannelButton.svelte'
|
||||||
|
export { default as StreamAddButton } from './StreamAddButton.svelte'
|
||||||
|
export { default as StreamAddIconButton } from './StreamAddIconButton.svelte'
|
||||||
|
export { default as StreamEditButton } from './StreamEditButton.svelte'
|
||||||
|
export { default as StreamReportButton } from './StreamReportButton.svelte'
|
||||||
export { default as StreamItem } from './StreamItem.svelte'
|
export { default as StreamItem } from './StreamItem.svelte'
|
||||||
|
export { default as StreamMenu } from './StreamMenu.svelte'
|
||||||
export { default as StreamsPopup } from './StreamsPopup.svelte'
|
export { default as StreamsPopup } from './StreamsPopup.svelte'
|
||||||
export { default as ToggleModeButton } from './ToggleModeButton.svelte'
|
export { default as ToggleModeButton } from './ToggleModeButton.svelte'
|
||||||
|
|
19
src/icons/AddCircle.svelte
Normal file
19
src/icons/AddCircle.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<svg
|
||||||
|
{...$$restProps}
|
||||||
|
width={$$props.size}
|
||||||
|
height={$$props.size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8V11H16C16.5523 11 17 11.4477 17 12C17 12.5523 16.5523 13 16 13H13V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V13H8C7.44771 13 7 12.5523 7 12C7 11.4477 7.44772 11 8 11H11V8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12ZM3.00683 12C3.00683 16.9668 7.03321 20.9932 12 20.9932C16.9668 20.9932 20.9932 16.9668 20.9932 12C20.9932 7.03321 16.9668 3.00683 12 3.00683C7.03321 3.00683 3.00683 7.03321 3.00683 12Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 834 B |
16
src/icons/Alert.svelte
Normal file
16
src/icons/Alert.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<svg
|
||||||
|
{...$$restProps}
|
||||||
|
width={$$props.size}
|
||||||
|
height={$$props.size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 434 B |
|
@ -1,4 +1,6 @@
|
||||||
export { default as Add } from './Add.svelte'
|
export { default as Add } from './Add.svelte'
|
||||||
|
export { default as AddCircle } from './AddCircle.svelte'
|
||||||
|
export { default as Alert } from './Alert.svelte'
|
||||||
export { default as CheckboxChecked } from './CheckboxChecked.svelte'
|
export { default as CheckboxChecked } from './CheckboxChecked.svelte'
|
||||||
export { default as CheckboxDisabled } from './CheckboxDisabled.svelte'
|
export { default as CheckboxDisabled } from './CheckboxDisabled.svelte'
|
||||||
export { default as CheckboxIndeterminate } from './CheckboxIndeterminate.svelte'
|
export { default as CheckboxIndeterminate } from './CheckboxIndeterminate.svelte'
|
||||||
|
|
|
@ -272,6 +272,10 @@ export class Channel {
|
||||||
return broadcastArea.uniqBy((broadcastArea: BroadcastArea) => broadcastArea.code)
|
return broadcastArea.uniqBy((broadcastArea: BroadcastArea) => broadcastArea.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeedNames(): Collection {
|
||||||
|
return this.getFeeds().map((feed: Feed) => feed.name)
|
||||||
|
}
|
||||||
|
|
||||||
getSearchable(): ChannelSearchable {
|
getSearchable(): ChannelSearchable {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -308,7 +312,8 @@ export class Channel {
|
||||||
_broadcastLocationNames: this.getBroadcastLocationNames().all(),
|
_broadcastLocationNames: this.getBroadcastLocationNames().all(),
|
||||||
_countryName: this.getCountryName(),
|
_countryName: this.getCountryName(),
|
||||||
_guideSiteNames: this.getGuideSiteNames().all(),
|
_guideSiteNames: this.getGuideSiteNames().all(),
|
||||||
_streamUrls: this.getStreamUrls().all()
|
_streamUrls: this.getStreamUrls().all(),
|
||||||
|
_feedNames: this.getFeedNames().all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,10 @@ export class Feed {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStreamId(): string {
|
||||||
|
return `${this.channelId}@${this.id}`
|
||||||
|
}
|
||||||
|
|
||||||
getUUID(): string {
|
getUUID(): string {
|
||||||
return this.channelId + this.id
|
return this.channelId + this.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { JsonDataViewerField } from '~/types/jsonDataViewerField'
|
|
||||||
import type { StreamData, StreamSerializedData } from '~/types/stream'
|
import type { StreamData, StreamSerializedData } from '~/types/stream'
|
||||||
|
import type { HTMLPreviewField } from '~/types/htmlPreviewField'
|
||||||
import type { Dictionary } from '@freearhey/core/browser'
|
import type { Dictionary } from '@freearhey/core/browser'
|
||||||
import { Link } from 'iptv-playlist-generator'
|
import { Link } from 'iptv-playlist-generator'
|
||||||
|
import type { Category } from './category'
|
||||||
import type { Channel } from './channel'
|
import type { Channel } from './channel'
|
||||||
import type { Feed } from './feed'
|
import type { Feed } from './feed'
|
||||||
import type { Category } from './category'
|
|
||||||
|
|
||||||
export class Stream {
|
export class Stream {
|
||||||
channelId?: string
|
channelId?: string
|
||||||
|
@ -54,31 +54,6 @@ export class Stream {
|
||||||
return `${this.channelId}@${this.feedId}`
|
return `${this.channelId}@${this.feedId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
channel: this.channelId,
|
|
||||||
feed: this.feedId,
|
|
||||||
url: this.url,
|
|
||||||
referrer: this.referrer,
|
|
||||||
user_agent: this.userAgent,
|
|
||||||
quality: this.quality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldset(): JsonDataViewerField[] {
|
|
||||||
let fieldset = []
|
|
||||||
|
|
||||||
const data = this.toJSON()
|
|
||||||
for (let key in data) {
|
|
||||||
fieldset.push({
|
|
||||||
name: key,
|
|
||||||
value: data[key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fieldset
|
|
||||||
}
|
|
||||||
|
|
||||||
getQuality(): string {
|
getQuality(): string {
|
||||||
if (!this.quality) return ''
|
if (!this.quality) return ''
|
||||||
|
|
||||||
|
@ -89,7 +64,7 @@ export class Stream {
|
||||||
return parseInt(this.getQuality().replace(/p|i/, ''))
|
return parseInt(this.getQuality().replace(/p|i/, ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle(): string {
|
getDisplayName(): string {
|
||||||
if (!this.channel) return ''
|
if (!this.channel) return ''
|
||||||
if (!this.feed) return this.channel.name
|
if (!this.feed) return this.channel.name
|
||||||
|
|
||||||
|
@ -101,7 +76,7 @@ export class Stream {
|
||||||
|
|
||||||
const link = new Link(this.url)
|
const link = new Link(this.url)
|
||||||
|
|
||||||
link.title = this.getTitle()
|
link.title = this.getDisplayName()
|
||||||
link.attrs = {
|
link.attrs = {
|
||||||
'tvg-id': this.getId(),
|
'tvg-id': this.getId(),
|
||||||
'tvg-logo': this.channel.logoUrl,
|
'tvg-logo': this.channel.logoUrl,
|
||||||
|
@ -125,6 +100,21 @@ export class Stream {
|
||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFieldset(): HTMLPreviewField[] {
|
||||||
|
return [
|
||||||
|
{ name: 'url', type: 'string', value: this.url, title: this.url },
|
||||||
|
this.referrer
|
||||||
|
? { name: 'referrer', type: 'string', value: this.referrer, title: this.referrer }
|
||||||
|
: null,
|
||||||
|
this.userAgent
|
||||||
|
? { name: 'user_agent', type: 'string', value: this.userAgent, title: this.userAgent }
|
||||||
|
: null,
|
||||||
|
this.quality
|
||||||
|
? { name: 'quality', type: 'string', value: this.quality, title: this.quality }
|
||||||
|
: null
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
serialize(): StreamSerializedData {
|
serialize(): StreamSerializedData {
|
||||||
return {
|
return {
|
||||||
channelId: this.channelId,
|
channelId: this.channelId,
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
ChannelRemoveButton,
|
ChannelRemoveButton,
|
||||||
ShareChannelButton,
|
ShareChannelButton,
|
||||||
ChannelEditButton,
|
ChannelEditButton,
|
||||||
|
FeedAddIconButton,
|
||||||
CopyLinkButton,
|
CopyLinkButton,
|
||||||
FeedAddButton,
|
|
||||||
BlockedBadge,
|
BlockedBadge,
|
||||||
HTMLPreview,
|
HTMLPreview,
|
||||||
ClosedBadge,
|
ClosedBadge,
|
||||||
|
@ -118,9 +118,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div slot="headerRight">
|
<div slot="headerRight">
|
||||||
<Menu bind:isOpened={isFeedMenuOpened}>
|
<FeedAddIconButton {channel} />
|
||||||
<FeedAddButton {channel} onClick={closeFeedMenu} />
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
<div slot="body">
|
<div slot="body">
|
||||||
<div class="flex flex-col gap-2 p-2 sm:p-5">
|
<div class="flex flex-col gap-2 p-2 sm:p-5">
|
||||||
|
|
2
src/types/channel.d.ts
vendored
2
src/types/channel.d.ts
vendored
|
@ -40,6 +40,7 @@ export type ChannelSearchable = {
|
||||||
_countryName: string
|
_countryName: string
|
||||||
_guideSiteNames: string[]
|
_guideSiteNames: string[]
|
||||||
_streamUrls: string[]
|
_streamUrls: string[]
|
||||||
|
_feedNames: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSerializedData = {
|
export type ChannelSerializedData = {
|
||||||
|
@ -60,6 +61,7 @@ export type ChannelSerializedData = {
|
||||||
launchedDate?: string
|
launchedDate?: string
|
||||||
closedDateString?: string
|
closedDateString?: string
|
||||||
closedDate?: string
|
closedDate?: string
|
||||||
|
replacedByStreamId?: string
|
||||||
replacedByChannelId?: string
|
replacedByChannelId?: string
|
||||||
websiteUrl?: string
|
websiteUrl?: string
|
||||||
logoUrl: string
|
logoUrl: string
|
||||||
|
|
|
@ -5,4 +5,5 @@ export type HTMLPreviewField = {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
value: HTMLPreviewImage | HTMLPreviewLink | HTMLPreviewLink[] | HTMLPreviewExternalLink | string
|
value: HTMLPreviewImage | HTMLPreviewLink | HTMLPreviewLink[] | HTMLPreviewExternalLink | string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"channel": "13MaxTelevision.ar",
|
"channel": "13MaxTelevision.ar",
|
||||||
"id": "SD",
|
"id": "Panregional",
|
||||||
"name": "SD",
|
"name": "Panregional",
|
||||||
"is_main": true,
|
"is_main": true,
|
||||||
"broadcast_area": [
|
"broadcast_area": [
|
||||||
"s/AR-W"
|
"s/AR-W"
|
||||||
|
|
340
tests/core/searchEngine.test.js
Normal file
340
tests/core/searchEngine.test.js
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
import { ApiClient, DataProcessor, DataLoader, SearchEngine } from '../../src/core'
|
||||||
|
import { expect, it, describe, beforeEach } from 'vitest'
|
||||||
|
import AxiosMockAdapter from 'axios-mock-adapter'
|
||||||
|
import axios from 'axios'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
const searchEngine = new SearchEngine()
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const client = new ApiClient()
|
||||||
|
const processor = new DataProcessor()
|
||||||
|
const dataLoader = new DataLoader({ client, processor })
|
||||||
|
|
||||||
|
client.instance = axios.create({
|
||||||
|
baseURL: 'https://iptv-org.github.io/api'
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockAxios = new AxiosMockAdapter(client.instance)
|
||||||
|
|
||||||
|
mockAxios.onGet(`categories.json`).reply(200, loadJson('categories.json'))
|
||||||
|
mockAxios.onGet(`countries.json`).reply(200, loadJson('countries.json'))
|
||||||
|
mockAxios.onGet(`languages.json`).reply(200, loadJson('languages.json'))
|
||||||
|
mockAxios.onGet(`blocklist.json`).reply(200, loadJson('blocklist.json'))
|
||||||
|
mockAxios.onGet(`timezones.json`).reply(200, loadJson('timezones.json'))
|
||||||
|
mockAxios.onGet(`channels.json`).reply(200, loadJson('channels.json'))
|
||||||
|
mockAxios.onGet(`regions.json`).reply(200, loadJson('regions.json'))
|
||||||
|
mockAxios.onGet(`streams.json`).reply(200, loadJson('streams.json'))
|
||||||
|
mockAxios.onGet(`guides.json`).reply(200, loadJson('guides.json'))
|
||||||
|
mockAxios.onGet(`feeds.json`).reply(200, loadJson('feeds.json'))
|
||||||
|
mockAxios.onGet(`subdivisions.json`).reply(200, loadJson('subdivisions.json'))
|
||||||
|
|
||||||
|
const data = await dataLoader.load()
|
||||||
|
const searchableData = data.channels.map(channel => channel.getSearchable())
|
||||||
|
|
||||||
|
searchEngine.createIndex(searchableData)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
it('returns empty list if there is no such channel', () => {
|
||||||
|
let results = searchEngine.search('lorem')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by name', () => {
|
||||||
|
let results = searchEngine.search('name:002')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by multiple words', () => {
|
||||||
|
let results = searchEngine.search('Xtrema Cartoons')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(2)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'XtremaCartoons.ar'
|
||||||
|
})
|
||||||
|
expect(results.all()[1]).toMatchObject({
|
||||||
|
id: 'XtremaRetroCartoons.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can search for one of two words', () => {
|
||||||
|
let results = searchEngine.search('Johannesburg,002')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(2)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
expect(results.all()[1]).toMatchObject({
|
||||||
|
id: 'FashionTVJohannesburg.fr'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can search for exact word matches', () => {
|
||||||
|
let results = searchEngine.search('"Xtrema Cartoons"')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'XtremaCartoons.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by id', () => {
|
||||||
|
let results = searchEngine.search('id:002RadioTV.do')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by feed name', () => {
|
||||||
|
let results = searchEngine.search('Panregional')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '13MaxTelevision.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by alternative names', () => {
|
||||||
|
let results = searchEngine.search('alt_names:التلفزيون')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'TV1.dz'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by network', () => {
|
||||||
|
let results = searchEngine.search('network:Hope')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'K11UUD1.as'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels without the owner', () => {
|
||||||
|
let results = searchEngine.search('owners:^$')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(7)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by country code', () => {
|
||||||
|
let results = searchEngine.search('country:DO')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels that are broadcast from the same region', () => {
|
||||||
|
let results = searchEngine.search('subdivision:AR-W')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '13MaxTelevision.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels that are broadcast from the same city', () => {
|
||||||
|
let results = searchEngine.search('city:Corrientes')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '13MaxTelevision.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels that have the same category', () => {
|
||||||
|
let results = searchEngine.search('categories:lifestyle')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'FashionTVJohannesburg.fr'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels with website', () => {
|
||||||
|
let results = searchEngine.search('website:.')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(14)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels marked as NSFW', () => {
|
||||||
|
let results = searchEngine.search('is_nsfw:true')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find closed channels', () => {
|
||||||
|
let results = searchEngine.search('is_closed:true')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'AynaTV.af'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find blocked channels', () => {
|
||||||
|
let results = searchEngine.search('is_blocked:true')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by query in lower case', () => {
|
||||||
|
let results = searchEngine.search('tv2')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(2)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'SEN502.us'
|
||||||
|
})
|
||||||
|
expect(results.all()[1]).toMatchObject({
|
||||||
|
id: 'CFCNTV2.ca'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by alternative name after another query', () => {
|
||||||
|
searchEngine.search('tv2')
|
||||||
|
let results = searchEngine.search('alt_names:tv2')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'SEN502.us'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels that have streams', () => {
|
||||||
|
let results = searchEngine.search('streams:>0')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'XtremaCartoons.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels that have guides', () => {
|
||||||
|
let results = searchEngine.search('guides:>0')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'LaLiganaZap.ao'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by country name', () => {
|
||||||
|
let results = searchEngine.search('"dominican republic"')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(3)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '002RadioTV.do'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by display name from the guides', () => {
|
||||||
|
let results = searchEngine.search('La Liga HD')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'LaLiganaZap.ao'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by stream url', () => {
|
||||||
|
let results = searchEngine.search(
|
||||||
|
'https://stmv6.voxtvhd.com.br/xtremacartoons/xtremacartoons/playlist.m3u8'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'XtremaCartoons.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by broadcast area code', () => {
|
||||||
|
let results = searchEngine.search('broadcast_area:s/AR-W')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: '13MaxTelevision.ar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by broadcast location code', () => {
|
||||||
|
let results = searchEngine.search('eur')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(2)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channel by broadcast location name', () => {
|
||||||
|
let results = searchEngine.search('europe')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(2)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by exact language code', () => {
|
||||||
|
let results = searchEngine.search('language:fra')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'SEN502.us'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by language name', () => {
|
||||||
|
let results = searchEngine.search('french')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'SEN502.us'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by video format', () => {
|
||||||
|
let results = searchEngine.search('video_format:576i')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find channels by timezone id', () => {
|
||||||
|
let results = searchEngine.search('timezone:Europe/London')
|
||||||
|
|
||||||
|
expect(results.count()).toBe(1)
|
||||||
|
expect(results.first()).toMatchObject({
|
||||||
|
id: 'Bizarre.al'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadJson(filepath) {
|
||||||
|
return JSON.parse(fs.readFileSync(path.resolve('tests/__data__/input/', filepath), 'utf8'))
|
||||||
|
}
|
|
@ -1,361 +0,0 @@
|
||||||
import { loadData, search, searchResults } from '../src/store'
|
|
||||||
import { expect, it, describe, beforeEach, afterEach, vi } from 'vitest'
|
|
||||||
import { get } from 'svelte/store'
|
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import AxiosMockAdapter from 'axios-mock-adapter'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { ApiClient, DataProcessor } from '../src/core'
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const client = new ApiClient()
|
|
||||||
const processor = new DataProcessor()
|
|
||||||
|
|
||||||
client.instance = axios.create({
|
|
||||||
baseURL: 'https://iptv-org.github.io/api'
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockAxios = new AxiosMockAdapter(client.instance)
|
|
||||||
|
|
||||||
mockAxios.onGet(`categories.json`).reply(200, loadJson('categories.json'))
|
|
||||||
mockAxios.onGet(`countries.json`).reply(200, loadJson('countries.json'))
|
|
||||||
mockAxios.onGet(`languages.json`).reply(200, loadJson('languages.json'))
|
|
||||||
mockAxios.onGet(`blocklist.json`).reply(200, loadJson('blocklist.json'))
|
|
||||||
mockAxios.onGet(`timezones.json`).reply(200, loadJson('timezones.json'))
|
|
||||||
mockAxios.onGet(`channels.json`).reply(200, loadJson('channels.json'))
|
|
||||||
mockAxios.onGet(`regions.json`).reply(200, loadJson('regions.json'))
|
|
||||||
mockAxios.onGet(`streams.json`).reply(200, loadJson('streams.json'))
|
|
||||||
mockAxios.onGet(`guides.json`).reply(200, loadJson('guides.json'))
|
|
||||||
mockAxios.onGet(`feeds.json`).reply(200, loadJson('feeds.json'))
|
|
||||||
mockAxios.onGet(`subdivisions.json`).reply(200, loadJson('subdivisions.json'))
|
|
||||||
|
|
||||||
await loadData({ client, processor })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('search', () => {
|
|
||||||
it('return all channels by default', () => {
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(15)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns empty list if there is no such channel', () => {
|
|
||||||
search('lorem')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by name', () => {
|
|
||||||
search('name:002')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by multiple words', () => {
|
|
||||||
search('Xtrema Cartoons')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(2)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'XtremaCartoons.ar'
|
|
||||||
})
|
|
||||||
expect(results[1]).toMatchObject({
|
|
||||||
id: 'XtremaRetroCartoons.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can search for one of two words', () => {
|
|
||||||
search('Johannesburg,002')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(2)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
expect(results[1]).toMatchObject({
|
|
||||||
id: 'FashionTVJohannesburg.fr'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can search for exact word matches', () => {
|
|
||||||
search('"Xtrema Cartoons"')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'XtremaCartoons.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by id', () => {
|
|
||||||
search('id:002RadioTV.do')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by alternative names', () => {
|
|
||||||
search('alt_names:التلفزيون')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'TV1.dz'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by network', () => {
|
|
||||||
search('network:Hope')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'K11UUD1.as'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels without the owner', () => {
|
|
||||||
search('owners:^$')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(7)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by country code', () => {
|
|
||||||
search('country:DO')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels that are broadcast from the same region', () => {
|
|
||||||
search('subdivision:AR-W')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '13MaxTelevision.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels that are broadcast from the same city', () => {
|
|
||||||
search('city:Corrientes')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '13MaxTelevision.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels that have the same category', () => {
|
|
||||||
search('categories:lifestyle')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'FashionTVJohannesburg.fr'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels with website', () => {
|
|
||||||
search('website:.')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(14)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels marked as NSFW', () => {
|
|
||||||
search('is_nsfw:true')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find closed channels', () => {
|
|
||||||
search('is_closed:true')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'AynaTV.af'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find blocked channels', () => {
|
|
||||||
search('is_blocked:true')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by query in lower case', () => {
|
|
||||||
search('tv2')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(2)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'SEN502.us'
|
|
||||||
})
|
|
||||||
expect(results[1]).toMatchObject({
|
|
||||||
id: 'CFCNTV2.ca'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by alternative name after another query', () => {
|
|
||||||
search('tv2')
|
|
||||||
search('alt_names:tv2')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'SEN502.us'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels that have streams', () => {
|
|
||||||
search('streams:>0')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'XtremaCartoons.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels that have guides', () => {
|
|
||||||
search('guides:>0')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'LaLiganaZap.ao'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by country name', () => {
|
|
||||||
search('"dominican republic"')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(3)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '002RadioTV.do'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by display name from the guides', () => {
|
|
||||||
search('La Liga HD')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'LaLiganaZap.ao'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by stream url', () => {
|
|
||||||
search('https://stmv6.voxtvhd.com.br/xtremacartoons/xtremacartoons/playlist.m3u8')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'XtremaCartoons.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by broadcast area code', () => {
|
|
||||||
search('broadcast_area:s/AR-W')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: '13MaxTelevision.ar'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by broadcast location code', () => {
|
|
||||||
search('eur')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(2)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channel by broadcast location name', () => {
|
|
||||||
search('europe')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(2)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by exact language code', () => {
|
|
||||||
search('language:fra')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'SEN502.us'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by language name', () => {
|
|
||||||
search('french')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'SEN502.us'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by video format', () => {
|
|
||||||
search('video_format:576i')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find channels by timezone id', () => {
|
|
||||||
search('timezone:Europe/London')
|
|
||||||
|
|
||||||
const results = get(searchResults).all()
|
|
||||||
expect(results.length).toBe(1)
|
|
||||||
expect(results[0]).toMatchObject({
|
|
||||||
id: 'Bizarre.al'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function loadJson(filepath) {
|
|
||||||
return JSON.parse(fs.readFileSync(path.resolve('tests/__data__/input/', filepath), 'utf8'))
|
|
||||||
}
|
|
13
yarn.lock
13
yarn.lock
|
@ -157,10 +157,10 @@
|
||||||
resolved "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz"
|
resolved "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz"
|
||||||
integrity sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==
|
integrity sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==
|
||||||
|
|
||||||
"@sveltejs/kit@^2.0.0", "@sveltejs/kit@^2.20.4":
|
"@sveltejs/kit@^2.0.0", "@sveltejs/kit@^2.20.7":
|
||||||
version "2.20.4"
|
version "2.20.7"
|
||||||
resolved "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.4.tgz"
|
resolved "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz"
|
||||||
integrity sha512-B3Y1mb1Qjt57zXLVch5tfqsK/ebHe6uYTcFSnGFNwRpId3+fplLgQK6Z2zhDVBezSsPuhDq6Pry+9PA88ocN6Q==
|
integrity sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/cookie" "^0.6.0"
|
"@types/cookie" "^0.6.0"
|
||||||
cookie "^0.6.0"
|
cookie "^0.6.0"
|
||||||
|
@ -1169,6 +1169,11 @@ run-parallel@^1.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
|
run-script-os@^1.1.6:
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz"
|
||||||
|
integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
|
||||||
|
|
||||||
sade@^1.8.1:
|
sade@^1.8.1:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
|
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue