mirror of
https://github.com/iptv-org/iptv-org.github.io.git
synced 2025-05-10 17:10:06 -04:00
Init
This commit is contained in:
parent
465c95db8a
commit
7fc7d5c0c2
35 changed files with 3806 additions and 1266 deletions
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
||||||
import{S as h,i as w,s as y,e as E,t as v,c as d,a as b,h as P,d as o,g as u,I as R,j as N,k as S,l as C,m as j,K as H}from"./chunks/vendor-dedc54d0.js";function I(r){let l,t=r[1].frame+"",a;return{c(){l=E("pre"),a=v(t)},l(f){l=d(f,"PRE",{});var s=b(l);a=P(s,t),s.forEach(o)},m(f,s){u(f,l,s),R(l,a)},p(f,s){s&2&&t!==(t=f[1].frame+"")&&N(a,t)},d(f){f&&o(l)}}}function K(r){let l,t=r[1].stack+"",a;return{c(){l=E("pre"),a=v(t)},l(f){l=d(f,"PRE",{});var s=b(l);a=P(s,t),s.forEach(o)},m(f,s){u(f,l,s),R(l,a)},p(f,s){s&2&&t!==(t=f[1].stack+"")&&N(a,t)},d(f){f&&o(l)}}}function z(r){let l,t,a,f,s=r[1].message+"",c,k,n,p,i=r[1].frame&&I(r),_=r[1].stack&&K(r);return{c(){l=E("h1"),t=v(r[0]),a=S(),f=E("pre"),c=v(s),k=S(),i&&i.c(),n=S(),_&&_.c(),p=C()},l(e){l=d(e,"H1",{});var m=b(l);t=P(m,r[0]),m.forEach(o),a=j(e),f=d(e,"PRE",{});var q=b(f);c=P(q,s),q.forEach(o),k=j(e),i&&i.l(e),n=j(e),_&&_.l(e),p=C()},m(e,m){u(e,l,m),R(l,t),u(e,a,m),u(e,f,m),R(f,c),u(e,k,m),i&&i.m(e,m),u(e,n,m),_&&_.m(e,m),u(e,p,m)},p(e,[m]){m&1&&N(t,e[0]),m&2&&s!==(s=e[1].message+"")&&N(c,s),e[1].frame?i?i.p(e,m):(i=I(e),i.c(),i.m(n.parentNode,n)):i&&(i.d(1),i=null),e[1].stack?_?_.p(e,m):(_=K(e),_.c(),_.m(p.parentNode,p)):_&&(_.d(1),_=null)},i:H,o:H,d(e){e&&o(l),e&&o(a),e&&o(f),e&&o(k),i&&i.d(e),e&&o(n),_&&_.d(e),e&&o(p)}}}function D({error:r,status:l}){return{props:{error:r,status:l}}}function A(r,l,t){let{status:a}=l,{error:f}=l;return r.$$set=s=>{"status"in s&&t(0,a=s.status),"error"in s&&t(1,f=s.error)},[a,f]}class F extends h{constructor(l){super();w(this,l,A,z,y,{status:0,error:1})}}export{F as default,D as load};
|
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
".svelte-kit/runtime/client/start.js": {
|
|
||||||
"file": "start-d23075dc.js",
|
|
||||||
"src": ".svelte-kit/runtime/client/start.js",
|
|
||||||
"isEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-dedc54d0.js"
|
|
||||||
],
|
|
||||||
"dynamicImports": [
|
|
||||||
"src/routes/__layout.svelte",
|
|
||||||
".svelte-kit/runtime/components/error.svelte",
|
|
||||||
"src/routes/index.svelte"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"src/routes/__layout.svelte": {
|
|
||||||
"file": "pages/__layout.svelte-6869b5a4.js",
|
|
||||||
"src": "src/routes/__layout.svelte",
|
|
||||||
"isEntry": true,
|
|
||||||
"isDynamicEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-dedc54d0.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
".svelte-kit/runtime/components/error.svelte": {
|
|
||||||
"file": "error.svelte-aa63ee3d.js",
|
|
||||||
"src": ".svelte-kit/runtime/components/error.svelte",
|
|
||||||
"isEntry": true,
|
|
||||||
"isDynamicEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-dedc54d0.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"src/routes/index.svelte": {
|
|
||||||
"file": "pages/index.svelte-1502efd3.js",
|
|
||||||
"src": "src/routes/index.svelte",
|
|
||||||
"isEntry": true,
|
|
||||||
"isDynamicEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-dedc54d0.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_vendor-dedc54d0.js": {
|
|
||||||
"file": "chunks/vendor-dedc54d0.js"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
import{S as Q,i as W,s as Z,F as x,G as ee,e as v,c as _,a as r,d as s,H as se,b as o,f as w,g as y,I as d,J as j,K as te,L as U,k as P,M as X,m as q,N as le,O as ae,P as oe,q as re,o as ie}from"../chunks/vendor-dedc54d0.js";function Y(b){let t,m,p,a,h;return{c(){t=v("button"),m=v("span"),p=v("ion-icon"),this.h()},l(i){t=_(i,"BUTTON",{class:!0,style:!0});var n=r(t);m=_(n,"SPAN",{class:!0});var g=r(m);p=_(g,"ION-ICON",{name:!0}),r(p).forEach(s),g.forEach(s),n.forEach(s),this.h()},h(){se(p,"name","arrow-up-outline"),o(m,"class","icon is-small"),o(t,"class","button level-item is-hidden-mobile"),w(t,"pointer-events","auto")},m(i,n){y(i,t,n),d(t,m),d(m,p),a||(h=j(t,"click",ce),a=!0)},p:te,d(i){i&&s(t),a=!1,h()}}}function ne(b){let t=!1,m=()=>{t=!1},p,a,h,i,n,g,T,D,N,$,c,O,E,V,S,I,k,A,F;x(b[3]);const B=b[2].default,u=ee(B,b,b[1],null);let l=b[0]>100&&Y();return{c(){a=v("div"),h=v("div"),i=v("div"),n=v("a"),g=v("span"),T=U("svg"),D=U("path"),N=P(),u&&u.c(),$=P(),c=v("footer"),O=v("div"),E=v("div"),V=v("div"),S=P(),I=v("div"),l&&l.c(),this.h()},l(e){a=_(e,"DIV",{class:!0,style:!0});var f=r(a);h=_(f,"DIV",{class:!0});var M=r(h);i=_(M,"DIV",{class:!0});var z=r(i);n=_(z,"A",{href:!0});var G=r(n);g=_(G,"SPAN",{class:!0});var H=r(g);T=X(H,"svg",{xmlns:!0,viewBox:!0});var J=r(T);D=X(J,"path",{d:!0}),r(D).forEach(s),J.forEach(s),H.forEach(s),G.forEach(s),z.forEach(s),M.forEach(s),f.forEach(s),N=q(e),u&&u.l(e),$=q(e),c=_(e,"FOOTER",{class:!0,style:!0});var K=r(c);O=_(K,"DIV",{class:!0});var L=r(O);E=_(L,"DIV",{class:!0});var C=r(E);V=_(C,"DIV",{class:!0}),r(V).forEach(s),S=q(C),I=_(C,"DIV",{class:!0});var R=r(I);l&&l.l(R),R.forEach(s),C.forEach(s),L.forEach(s),K.forEach(s),this.h()},h(){o(D,"d","M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z"),o(T,"xmlns","http://www.w3.org/2000/svg"),o(T,"viewBox","0 0 512 512"),o(g,"class","icon"),o(n,"href","https://github.com/iptv-org/api"),o(i,"class","navbar-item"),o(h,"class","navbar-end"),o(a,"class","navbar"),w(a,"background-color","transparent"),o(V,"class","level-left"),o(I,"class","level-right"),o(E,"class","level"),o(O,"class","content"),o(c,"class","footer"),w(c,"background-color","transparent"),w(c,"position","fixed"),w(c,"bottom","0"),w(c,"width","100%"),w(c,"padding","3rem 1.5rem"),w(c,"pointer-events","none")},m(e,f){y(e,a,f),d(a,h),d(h,i),d(i,n),d(n,g),d(g,T),d(T,D),y(e,N,f),u&&u.m(e,f),y(e,$,f),y(e,c,f),d(c,O),d(O,E),d(E,V),d(E,S),d(E,I),l&&l.m(I,null),k=!0,A||(F=j(window,"scroll",()=>{t=!0,clearTimeout(p),p=setTimeout(m,100),b[3]()}),A=!0)},p(e,[f]){f&1&&!t&&(t=!0,clearTimeout(p),scrollTo(window.pageXOffset,e[0]),p=setTimeout(m,100)),u&&u.p&&(!k||f&2)&&le(u,B,e,e[1],k?oe(B,e[1],f,null):ae(e[1]),null),e[0]>100?l?l.p(e,f):(l=Y(),l.c(),l.m(I,null)):l&&(l.d(1),l=null)},i(e){k||(re(u,e),k=!0)},o(e){ie(u,e),k=!1},d(e){e&&s(a),e&&s(N),u&&u.d(e),e&&s($),e&&s(c),l&&l.d(),A=!1,F()}}}function ce(b){document.body.scrollTop=0,document.documentElement.scrollTop=0}function ue(b,t,m){let{$$slots:p={},$$scope:a}=t,h=0;function i(){m(0,h=window.pageYOffset)}return b.$$set=n=>{"$$scope"in n&&m(1,a=n.$$scope)},[h,a,p,i]}class de extends Q{constructor(t){super();W(this,t,ue,ne,Z,{})}}export{de as default};
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
||||||
{"version":"1645521886104"}
|
|
BIN
docs/favicon.png
BIN
docs/favicon.png
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
|
@ -1,58 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="description" content="" />
|
|
||||||
<link rel="icon" href="./favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" />
|
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"
|
|
||||||
></script>
|
|
||||||
<meta http-equiv="content-security-policy" content="">
|
|
||||||
<link rel="modulepreload" href="/_app/start-d23075dc.js">
|
|
||||||
<link rel="modulepreload" href="/_app/chunks/vendor-dedc54d0.js">
|
|
||||||
<link rel="modulepreload" href="/_app/pages/__layout.svelte-6869b5a4.js">
|
|
||||||
<link rel="modulepreload" href="/_app/pages/index.svelte-1502efd3.js">
|
|
||||||
</head>
|
|
||||||
<body style="background-color: #f6f8fa; min-height: 100vh">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="navbar" style="background-color: transparent"><div class="navbar-end"><div class="navbar-item"><a href="https://github.com/iptv-org/api"><span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z"></path></svg></span></a></div></div></div>
|
|
||||||
|
|
||||||
<div class="section"><div class="container"><div class="columns is-centered"><div class="column is-9"><form class="mb-5"><div class="field-body"><div class="field is-expanded"><div class="field has-addons"><div class="control is-expanded"><input class="input" type="search" placeholder="Search by channel name..." value=""></div>
|
|
||||||
<div class="control"><button class="button is-info" type="submit"><span class="icon is-small is-right"><svg xmlns="http://www.w3.org/2000/svg" style="width: 1.25rem; height: 1.25rem" viewBox="0 0 512 512"><path fill="#ffffff" d="M456.69 421.39L362.6 327.3a173.81 173.81 0 0034.84-104.58C397.44 126.38 319.06 48 222.72 48S48 126.38 48 222.72s78.38 174.72 174.72 174.72A173.81 173.81 0 00327.3 362.6l94.09 94.09a25 25 0 0035.3-35.3zM97.92 222.72a124.8 124.8 0 11124.8 124.8 124.95 124.95 0 01-124.8-124.8z"></path></svg></span></button></div></div>
|
|
||||||
</div></div></form>
|
|
||||||
|
|
||||||
<div class="level"><div class="level-item">Loading...</div></div> </div></div></div></div>
|
|
||||||
|
|
||||||
<footer class="footer" style="background-color: transparent; position: fixed; bottom: 0; width: 100%; padding: 3rem 1.5rem; pointer-events: none; "><div class="content"><div class="level"><div class="level-left"></div>
|
|
||||||
<div class="level-right"></div></div></div></footer>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="module" data-hydrate="299air">
|
|
||||||
import { start } from "/_app/start-d23075dc.js";
|
|
||||||
start({
|
|
||||||
target: document.querySelector('[data-hydrate="299air"]').parentNode,
|
|
||||||
paths: {"base":"","assets":""},
|
|
||||||
session: {},
|
|
||||||
route: true,
|
|
||||||
spa: false,
|
|
||||||
trailing_slash: "never",
|
|
||||||
hydrate: {
|
|
||||||
status: 200,
|
|
||||||
error: null,
|
|
||||||
nodes: [
|
|
||||||
import("/_app/pages/__layout.svelte-6869b5a4.js"),
|
|
||||||
import("/_app/pages/index.svelte-1502efd3.js")
|
|
||||||
],
|
|
||||||
params: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -6,5 +6,6 @@
|
||||||
"$lib/*": ["src/lib/*"]
|
"$lib/*": ["src/lib/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json"
|
||||||
}
|
}
|
||||||
|
|
3298
package-lock.json
generated
3298
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "site",
|
"name": "iptv-org",
|
||||||
"version": "0.0.1",
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "svelte-kit dev",
|
"dev": "svelte-kit dev",
|
||||||
"build": "svelte-kit build",
|
"build": "svelte-kit build",
|
||||||
|
@ -11,9 +11,17 @@
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "next",
|
||||||
|
"@zerodevx/svelte-json-view": "^0.2.0",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"postcss": "^8.4.6",
|
||||||
"prettier-plugin-svelte": "^2.6.0",
|
"prettier-plugin-svelte": "^2.6.0",
|
||||||
"svelte": "^3.44.0"
|
"svelte": "^3.44.0",
|
||||||
|
"svelte-clipboard": "^1.0.0",
|
||||||
|
"svelte-infinite-loading": "^1.3.8",
|
||||||
|
"svelte-simple-modal": "^1.3.1",
|
||||||
|
"tailwindcss": "^3.0.23",
|
||||||
|
"transliteration": "^2.2.0"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|
3
src/app.css
Normal file
3
src/app.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
10
src/app.html
10
src/app.html
|
@ -2,17 +2,11 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="description" content="" />
|
|
||||||
<link rel="icon" href="%svelte.assets%/favicon.png" />
|
<link rel="icon" href="%svelte.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" />
|
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"
|
|
||||||
></script>
|
|
||||||
%svelte.head%
|
%svelte.head%
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #f6f8fa; min-height: 100vh">
|
<body>
|
||||||
<div>%svelte.body%</div>
|
%svelte.body%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
43
src/components/ChannelGrid.svelte
Normal file
43
src/components/ChannelGrid.svelte
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import ChannelItem from './ChannelItem.svelte'
|
||||||
|
|
||||||
|
export let channels = []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<div class="inline-block min-w-full align-middle">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<table
|
||||||
|
class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 table-fixed md:w-[62rem]"
|
||||||
|
>
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="min-w-[10rem] md:w-[12rem]"></th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="py-3 px-2 text-xs font-semibold tracking-wider text-left text-gray-400 uppercase dark:text-gray-400 min-w-[14rem] md:w-[19rem]"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="py-3 px-2 text-xs font-semibold tracking-wider text-left text-gray-400 uppercase dark:text-gray-400 min-w-[12rem] md:w-[18rem]"
|
||||||
|
>
|
||||||
|
TVG-ID
|
||||||
|
</th>
|
||||||
|
<th scope="col">
|
||||||
|
<span class="sr-only">Actions</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white dark:bg-gray-800">
|
||||||
|
{#each channels as channel}
|
||||||
|
<ChannelItem bind:channel="{channel}" />
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,30 +1,121 @@
|
||||||
<script>
|
<script>
|
||||||
export let channel
|
import { getContext } from 'svelte'
|
||||||
|
import StreamsPopup from './StreamsPopup.svelte'
|
||||||
|
import GuidesPopup from './GuidesPopup.svelte'
|
||||||
|
import ChannelPopup from './ChannelPopup.svelte'
|
||||||
|
|
||||||
|
export let channel
|
||||||
|
|
||||||
|
const guides = channel._guides
|
||||||
|
const streams = channel._streams
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal')
|
||||||
|
const showGuides = () =>
|
||||||
|
open(
|
||||||
|
GuidesPopup,
|
||||||
|
{ guides, title: channel.name },
|
||||||
|
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
||||||
|
)
|
||||||
|
const showStreams = () =>
|
||||||
|
open(
|
||||||
|
StreamsPopup,
|
||||||
|
{ streams, title: channel.name },
|
||||||
|
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
||||||
|
)
|
||||||
|
const showChannelData = () => {
|
||||||
|
open(
|
||||||
|
ChannelPopup,
|
||||||
|
{ channel },
|
||||||
|
{ transitionBgProps: { duration: 0 }, transitionWindowProps: { duration: 0 } }
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr>
|
<tr
|
||||||
<td class="is-vcentered" style="min-width: 150px; text-align: center">
|
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"
|
||||||
{#if channel && channel.logo}
|
>
|
||||||
<img
|
<td class="pl-2 pr-4 md:pr-7">
|
||||||
loading="lazy"
|
<div class="inline-flex w-full align-middle justify-center whitespace-nowrap overflow-hidden">
|
||||||
referrerpolicy="no-referrer"
|
{#if channel.logo}
|
||||||
src="{channel.logo}"
|
<img
|
||||||
alt="{channel.name}"
|
class="block align-middle mx-auto max-w-[6rem] max-h-[3rem] text-sm text-gray-400 dark:text-gray-600 cursor-default"
|
||||||
style="max-width: 100px; max-height: 50px; vertical-align: middle"
|
loading="lazy"
|
||||||
/>
|
referrerpolicy="no-referrer"
|
||||||
{/if}
|
src="{channel.logo}"
|
||||||
</td>
|
alt="{channel.name}"
|
||||||
<td class="is-vcentered" nowrap>
|
/>
|
||||||
<p>{channel.name}</p>
|
{/if}
|
||||||
</td>
|
</div>
|
||||||
<td class="is-vcentered" nowrap>
|
</td>
|
||||||
<code style="user-select: all">{channel.id}</code>
|
<td class="px-2">
|
||||||
</td>
|
<div>
|
||||||
<td class="is-vcentered">
|
<a
|
||||||
{#each channel.guides as guide}
|
on:click|preventDefault="{showChannelData}"
|
||||||
<p>
|
href="/"
|
||||||
<code style="white-space: nowrap; user-select: all">{guide.url}</code>
|
rel="nofollow"
|
||||||
</p>
|
role="button"
|
||||||
{/each}
|
tabindex="0"
|
||||||
</td>
|
class="text-left font-normal text-gray-600 dark:text-white hover:underline hover:text-blue-500"
|
||||||
|
>
|
||||||
|
{channel.name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-2">
|
||||||
|
<div>
|
||||||
|
<code
|
||||||
|
class="break-words text-sm text-gray-500 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>
|
||||||
|
<td class="pl-2 pr-5">
|
||||||
|
<div class="text-right flex justify-end space-x-3 items-center">
|
||||||
|
{#if streams.length}
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
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"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="font-semibold">{streams.length}</div>
|
||||||
|
<div>streams</div>
|
||||||
|
</button>
|
||||||
|
{/if}{#if guides.length}
|
||||||
|
<button
|
||||||
|
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
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="font-semibold">{guides.length}</div>
|
||||||
|
<div>guides</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
90
src/components/ChannelPopup.svelte
Normal file
90
src/components/ChannelPopup.svelte
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<script>
|
||||||
|
import JsonDataViewer from './JsonDataViewer.svelte'
|
||||||
|
import HTMLPreview from './HTMLPreview.svelte'
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
const { close } = getContext('simple-modal')
|
||||||
|
|
||||||
|
export let channel
|
||||||
|
|
||||||
|
let view = 'html'
|
||||||
|
function switchView(value) {
|
||||||
|
view = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.active {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #111828;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="relative px-2 py-[4rem] flex justify-center" on:click|self="{closePopup}">
|
||||||
|
<div class="relative bg-white rounded-md shadow dark:bg-gray-800 w-full max-w-4xl">
|
||||||
|
<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">{channel.name}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex justify-center w-1/3">
|
||||||
|
<div class="inline-flex rounded-md" role="group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
area-selected="{view === 'html'}"
|
||||||
|
on:click="{() => switchView('html')}"
|
||||||
|
class:active="{view === 'html'}"
|
||||||
|
class="py-2 px-4 text-xs font-medium text-gray-900 bg-white rounded-l-lg border border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:bg-transparent dark:text-white dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
HTML
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
area-selected="{view === 'html'}"
|
||||||
|
on:click="{() => switchView('json')}"
|
||||||
|
class:active="{view === 'json'}"
|
||||||
|
class="py-2 px-4 text-xs font-medium text-gray-900 bg-white border-t border-b border-r rounded-r-lg border-gray-200 hover:bg-gray-100 dark:bg-transparent dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-flex w-1/3 justify-end">
|
||||||
|
<button
|
||||||
|
on:click="{closePopup}"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-full text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-scroll overflow-x-hidden w-full">
|
||||||
|
{#if view === 'json'}
|
||||||
|
<div class="pb-8 px-8 pt-6">
|
||||||
|
<div class="flex p-4 bg-gray-50 dark:bg-gray-700 rounded-md w-full">
|
||||||
|
<JsonDataViewer data="{channel._raw}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if view === 'html'}
|
||||||
|
<HTMLPreview data="{channel}" close="{closePopup}" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
57
src/components/CopyToClipboard.svelte
Normal file
57
src/components/CopyToClipboard.svelte
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<script>
|
||||||
|
import Clipboard from 'svelte-clipboard'
|
||||||
|
|
||||||
|
export let text
|
||||||
|
let showTooltip = false
|
||||||
|
|
||||||
|
function onSuccess() {
|
||||||
|
showTooltip = true
|
||||||
|
setTimeout(() => {
|
||||||
|
showTooltip = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tooltip::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
border-width: 7px;
|
||||||
|
border-style: solid;
|
||||||
|
transform: translate3d(0, -7px, 0px);
|
||||||
|
border-color: transparent transparent transparent black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<Clipboard text="{text}" on:copy="{onSuccess}" let:copy>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click="{copy}"
|
||||||
|
class="relative flex items-center p-1 text-xs text-gray-500 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span class="hidden">Copy to Clipboard</span>
|
||||||
|
<div
|
||||||
|
role="tooltip"
|
||||||
|
class:hidden="{!showTooltip}"
|
||||||
|
class="tooltip inline-block absolute right-10 top-0 py-2 px-3 text-xs text-gray-100 rounded-md bg-black"
|
||||||
|
>
|
||||||
|
Copied!
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Clipboard>
|
|
@ -1,65 +1,52 @@
|
||||||
<script>
|
<script>
|
||||||
import ChannelItem from './ChannelItem.svelte'
|
import ChannelGrid from './ChannelGrid.svelte'
|
||||||
|
|
||||||
export let country
|
export let country
|
||||||
export let channels = []
|
export let channels = []
|
||||||
export let normQuery
|
export let hasQuery
|
||||||
|
|
||||||
|
$: expanded = country.expanded || (channels && channels.length > 0 && hasQuery)
|
||||||
|
|
||||||
function onExpand() {
|
function onExpand() {
|
||||||
country.expanded = !country.expanded
|
country.expanded = !country.expanded
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if channels && channels.length > 0}
|
<div class="mb-3">
|
||||||
<div class="card mb-3 is-shadowless" style="border: 1px solid #dbdbdb">
|
<h2 id="accordion-heading-{country.code}">
|
||||||
<div class="card-header is-shadowless is-clickable" on:click="{onExpand}">
|
<button
|
||||||
<span class="card-header-title">{country.flag} {country.name}</span>
|
on:click="{onExpand}"
|
||||||
<button class="card-header-icon" aria-label="more options">
|
type="button"
|
||||||
<span class="icon">
|
class="flex items-center focus:ring-0 dark:focus:ring-gray-800 justify-between p-4 w-full font-medium text-left border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512">
|
class:rounded-t-md="{expanded}"
|
||||||
{#if !country.expanded}
|
class:rounded-md="{!expanded}"
|
||||||
<path
|
class:border-b-0="{expanded}"
|
||||||
fill="none"
|
aria-expanded="{expanded}"
|
||||||
stroke="currentColor"
|
aria-controls="accordion-body-{country.code}"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<span>{country.flag} {country.name}</span>
|
||||||
stroke-width="48"
|
<svg
|
||||||
d="M112 184l144 144 144-144"
|
class:rotate-180="{expanded}"
|
||||||
/>
|
class="w-6 h-6 shrink-0"
|
||||||
{/if} {#if country.expanded}
|
fill="currentColor"
|
||||||
<path
|
viewBox="0 0 20 20"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke="currentColor"
|
>
|
||||||
stroke-linecap="round"
|
<path
|
||||||
stroke-linejoin="round"
|
fill-rule="evenodd"
|
||||||
stroke-width="48"
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
d="M112 328l144-144 144 144"
|
clip-rule="evenodd"
|
||||||
/>
|
></path>
|
||||||
{/if}
|
</svg>
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</h2>
|
||||||
{#if country.expanded || (channels && channels.length > 0 && normQuery.length)}
|
{#if expanded}
|
||||||
<div class="card-content">
|
<div id="accordion-body-{country.code}" aria-labelledby="accordion-heading-{country.code}">
|
||||||
<div class="table-container">
|
<div
|
||||||
<table class="table" style="min-width: 100%">
|
class="border border-gray-200 dark:border-gray-700 dark:bg-gray-900 rounded-b-md overflow-hidden"
|
||||||
<thead>
|
>
|
||||||
<tr>
|
<ChannelGrid bind:channels="{channels}" />
|
||||||
<th></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>TVG-ID</th>
|
|
||||||
<th>EPG</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each channels as channel}
|
|
||||||
<ChannelItem bind:channel="{channel}"></ChannelItem>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
73
src/components/GuideItem.svelte
Normal file
73
src/components/GuideItem.svelte
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||||
|
import JsonDataViewer from './JsonDataViewer.svelte'
|
||||||
|
|
||||||
|
export let guide
|
||||||
|
|
||||||
|
let expanded = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full bg-gray-100 dark:bg-gray-700 dark:border-gray-600 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full inline-flex justify-between px-3 py-2 border-gray-200 dark:border-gray-600"
|
||||||
|
class:border-b="{expanded}"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-3 items-center max-w-[90%]">
|
||||||
|
<button
|
||||||
|
class="w-4 h-4 flex justify-center align-middle text-gray-500 hover:text-blue-600 dark:text-gray-100 dark:hover:text-blue-600 shrink-0"
|
||||||
|
on:click="{() => {expanded = !expanded}}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
class:rotate-90="{expanded}"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
class="whitespace-nowrap text-sm text-gray-600 dark:text-gray-100 hover:text-blue-500 hover:underline inline-flex align-middle"
|
||||||
|
href="{guide.url}"
|
||||||
|
title="{guide.url}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="truncate max-w-[30rem]">{guide.url}</span
|
||||||
|
><span
|
||||||
|
class="inline-flex items-center pl-1 text-sm font-semibold text-gray-500 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
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>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="flex shrink-0">
|
||||||
|
<CopyToClipboard text="{guide.url}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if expanded}
|
||||||
|
<div class="w-full flex px-2 py-4">
|
||||||
|
<JsonDataViewer data="{guide}" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
62
src/components/GuidesPopup.svelte
Normal file
62
src/components/GuidesPopup.svelte
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<script>
|
||||||
|
import GuideItem from './GuideItem.svelte'
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
const { close } = getContext('simple-modal')
|
||||||
|
|
||||||
|
export let guides = []
|
||||||
|
export let title = 'Guides'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative px-2 py-[9rem] flex justify-center" on:click|self="{close}">
|
||||||
|
<div class="relative bg-white rounded-md shadow dark:bg-gray-800 w-full max-w-2xl">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center py-4 pl-5 pr-4 rounded-t border-b dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<h3 class="text-l font-medium text-gray-800 dark:text-white inline-flex items-center">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center pr-2 text-sm font-semibold text-gray-500 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg> </span
|
||||||
|
>{title}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
on:click="{close}"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-full text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-scroll overflow-x-hidden w-full">
|
||||||
|
<div class="p-6 space-y-2">
|
||||||
|
{#each guides as guide}
|
||||||
|
<GuideItem guide="{guide}" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
102
src/components/HTMLPreview.svelte
Normal file
102
src/components/HTMLPreview.svelte
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import { search, query, hasQuery } from '../store.js'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
export let close
|
||||||
|
|
||||||
|
const fieldset = [
|
||||||
|
{ name: 'logo', type: 'image', value: data.logo },
|
||||||
|
{ name: 'name', type: 'string', value: data.name },
|
||||||
|
{ name: 'native_name', type: 'string', value: data.native_name },
|
||||||
|
{ name: 'network', type: 'link', value: data.network },
|
||||||
|
{ name: 'country', type: 'link', value: data.country.name },
|
||||||
|
{ name: 'subdivision', type: 'link', value: data.subdivision ? data.subdivision.name : null },
|
||||||
|
{ name: 'city', type: 'link', value: data.city },
|
||||||
|
{ name: 'broadcast_area', type: 'link[]', value: data.broadcast_area.map(v => v.name) },
|
||||||
|
{ name: 'languages', type: 'link[]', value: data.languages.map(v => v.name) },
|
||||||
|
{ name: 'categories', type: 'link[]', value: data.categories.map(v => v.name) },
|
||||||
|
{ name: 'is_nsfw', type: 'link', value: data.is_nsfw.toString() },
|
||||||
|
{ name: 'website', type: 'external_link', value: data.website }
|
||||||
|
].filter(f => (Array.isArray(f.value) ? f.value.length : f.value))
|
||||||
|
|
||||||
|
function searchBy(name, value) {
|
||||||
|
value = value.includes(' ') ? `"${value}"` : value
|
||||||
|
const q = `${name}:${value}`
|
||||||
|
query.set(q)
|
||||||
|
hasQuery.set(true)
|
||||||
|
search(q)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="pb-8 px-8 pt-6 dark:text-white">
|
||||||
|
<div class="flex p-4 w-full">
|
||||||
|
<table class="table-fixed w-full">
|
||||||
|
<tbody>
|
||||||
|
{#each fieldset as field}
|
||||||
|
<tr>
|
||||||
|
<td class="align-top w-[11rem]">
|
||||||
|
<div class="flex px-4 py-1 text-sm text-gray-400 whitespace-nowrap dark:text-gray-400">
|
||||||
|
{field.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="align-top">
|
||||||
|
<div class="flex px-4 py-1 text-sm text-gray-700 dark:text-gray-100 flex-wrap">
|
||||||
|
{#if field.type === 'image'}
|
||||||
|
<img
|
||||||
|
src="{field.value}"
|
||||||
|
alt="{field.name}"
|
||||||
|
loading="lazy"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
class="border rounded-sm overflow-hidden border-gray-200 bg-[#e6e6e6]"
|
||||||
|
/>
|
||||||
|
{:else if field.type === 'link'}
|
||||||
|
<button
|
||||||
|
on:click="{() => searchBy(field.name, field.value)}"
|
||||||
|
class="underline hover:text-blue-500"
|
||||||
|
>
|
||||||
|
{field.value}
|
||||||
|
</button>
|
||||||
|
{:else if field.type === 'link[]'} {#each field.value as value, i} {#if i > 0}<span
|
||||||
|
>,
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
on:click="{() => searchBy(field.name, value)}"
|
||||||
|
class="underline hover:text-blue-500"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
{/each} {:else if field.type === 'external_link'}
|
||||||
|
<a
|
||||||
|
href="{field.value}"
|
||||||
|
class="underline hover:text-blue-500 inline-flex align-middle"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>{field.value}<span
|
||||||
|
class="inline-flex items-center pl-1 text-sm font-semibold text-gray-400 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
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>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
{:else} {field.value} {/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
80
src/components/JsonDataViewer.svelte
Normal file
80
src/components/JsonDataViewer.svelte
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { JsonView } from '@zerodevx/svelte-json-view'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
let dark = false
|
||||||
|
|
||||||
|
let fieldset = []
|
||||||
|
for (let key in data) {
|
||||||
|
if (key.startsWith('_')) continue
|
||||||
|
fieldset.push({
|
||||||
|
name: key,
|
||||||
|
value: data[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (
|
||||||
|
localStorage.getItem('color-theme') === 'light' ||
|
||||||
|
(!('color-theme' in localStorage) &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
|
dark = false
|
||||||
|
} else {
|
||||||
|
dark = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.value .val),
|
||||||
|
:global(.value .key) {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||||
|
'Courier New', monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark .value) {
|
||||||
|
--leafDefaultColor: white;
|
||||||
|
--leafStringColor: white;
|
||||||
|
--leafNumberColor: white;
|
||||||
|
--leafBooleanColor: white;
|
||||||
|
--nodeColor: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.value) {
|
||||||
|
--nodeBorderLeft: 1px dotted #9ca3b0;
|
||||||
|
--leafDefaultColor: #525a69;
|
||||||
|
--leafStringColor: #525a69;
|
||||||
|
--leafNumberColor: #525a69;
|
||||||
|
--leafBooleanColor: #525a69;
|
||||||
|
--nodeColor: #525a69;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.value .key),
|
||||||
|
:global(.value .comma) {
|
||||||
|
color: #9ca3b0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<table class="table-fixed w-full dark:text-white">
|
||||||
|
<tbody>
|
||||||
|
{#each fieldset as field}
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="w-[6rem] md:w-[11rem] px-4 py-1 text-sm text-gray-400 whitespace-nowrap dark:text-gray-400 align-top"
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-1 text-sm text-gray-600 dark:text-gray-100 align-top value break-words">
|
||||||
|
{#if Array.isArray(field.value) && field.value.length}
|
||||||
|
<JsonView json="{field.value}" />
|
||||||
|
{:else}
|
||||||
|
<code>{JSON.stringify(field.value)}</code>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
194
src/components/NavBar.svelte
Normal file
194
src/components/NavBar.svelte
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
<script>
|
||||||
|
import { query, hasQuery, search } from '../store.js'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import SearchFieldMini from './SearchFieldMini.svelte'
|
||||||
|
|
||||||
|
export let withSearch = false
|
||||||
|
|
||||||
|
let dark = false
|
||||||
|
function toggleDarkMode() {
|
||||||
|
let mode = localStorage.theme || 'light'
|
||||||
|
if (mode === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
dark = false
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
localStorage.theme = 'light'
|
||||||
|
} else {
|
||||||
|
dark = true
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
localStorage.theme = 'dark'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
document.body.scrollIntoView()
|
||||||
|
query.set('')
|
||||||
|
hasQuery.set(false)
|
||||||
|
search('')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let mode = localStorage.theme || 'light'
|
||||||
|
if (mode === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
dark = true
|
||||||
|
} else {
|
||||||
|
dark = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
class="bg-white border-b border-gray-200 px-2 sm:px-4 py-2.5 dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="container flex justify-between items-center max-w-6xl mx-auto px-4 sm:px-2">
|
||||||
|
<div class="flex flex-start items-center basis-[24rem] shrink">
|
||||||
|
<a href="/" on:click="{() => {reset()}}" class="flex mr-6">
|
||||||
|
<span
|
||||||
|
class="text-[1.15rem] text-[#24292f] self-center font-semibold whitespace-nowrap dark:text-white font-mono"
|
||||||
|
>/iptv-org</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{#if withSearch}
|
||||||
|
<SearchFieldMini />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-end items-center">
|
||||||
|
<div class="md:inline-block md:w-auto pr-4">
|
||||||
|
<ul class="hidden lg:flex space-x-7">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/iptv-org/iptv"
|
||||||
|
class="block py-2 pr-4 pl-3 text-sm text-gray-700 border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
target="_blank"
|
||||||
|
>Playlists<span
|
||||||
|
class="inline-flex items-center p-1 mr-2 text-sm font-semibold text-gray-400 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 fill-gray-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||||||
|
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"></path>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/iptv-org/epg"
|
||||||
|
class="block py-2 pr-4 pl-3 text-sm text-gray-700 border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
target="_blank"
|
||||||
|
>EPG<span
|
||||||
|
class="inline-flex items-center p-1 mr-2 text-sm font-semibold text-gray-400 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 fill-gray-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||||||
|
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"></path>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/iptv-org/database"
|
||||||
|
class="block py-2 pr-4 pl-3 text-sm text-gray-700 border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
target="_blank"
|
||||||
|
>Database<span
|
||||||
|
class="inline-flex items-center p-1 mr-2 text-sm font-semibold text-gray-400 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 fill-gray-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||||||
|
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"></path>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/iptv-org/api"
|
||||||
|
class="block py-2 pr-4 pl-3 text-sm text-gray-700 border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
target="_blank"
|
||||||
|
>API<span
|
||||||
|
class="inline-flex items-center p-1 mr-2 text-sm font-semibold text-gray-400 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 fill-gray-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||||||
|
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"></path>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click="{toggleDarkMode}"
|
||||||
|
class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full text-sm p-2.5"
|
||||||
|
aria-label="Toggle Dark Mode"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
class:hidden="{dark}"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
class:hidden="{!dark}"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://github.com/iptv-org/"
|
||||||
|
class="inline-flex text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full text-sm p-2.5 ml-1"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="GitHub"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 496 512"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
42
src/components/SearchField.svelte
Normal file
42
src/components/SearchField.svelte
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<script>
|
||||||
|
import { query, search } from '../store.js'
|
||||||
|
|
||||||
|
export let found = 0
|
||||||
|
export let isLoading = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="mb-5" on:submit|preventDefault="{search($query)}">
|
||||||
|
<div>
|
||||||
|
<label for="search-input" class="sr-only">Search</label>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="search-input"
|
||||||
|
bind:value="{$query}"
|
||||||
|
class="bg-white border border-gray-300 text-gray-900 outline-blue-500 text-sm rounded-md block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
placeholder="Search for channels"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">
|
||||||
|
<span class="inline-flex text-sm text-gray-500 dark:text-gray-400 font-mono"
|
||||||
|
>Found
|
||||||
|
<span class:animate-spin="{isLoading}">{ !isLoading ? found.toLocaleString() : '/' }</span>
|
||||||
|
channels</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
32
src/components/SearchFieldMini.svelte
Normal file
32
src/components/SearchFieldMini.svelte
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { query, search } from '../store.js'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault="{search($query)}" autocomplete="off" class="w-full">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="search-input" class="sr-only">Search</label>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-500 dark:text-gray-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="search-input"
|
||||||
|
bind:value="{$query}"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 outline-blue-500 text-sm rounded-md block w-full pl-9 p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
84
src/components/StreamItem.svelte
Normal file
84
src/components/StreamItem.svelte
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||||
|
import JsonDataViewer from './JsonDataViewer.svelte'
|
||||||
|
|
||||||
|
export let stream
|
||||||
|
|
||||||
|
let expanded = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full bg-gray-100 dark:bg-gray-700 dark:border-gray-600 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full inline-flex justify-between px-3 py-2 border-gray-200 dark:border-gray-600"
|
||||||
|
class:border-b="{expanded}"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-3 items-center max-w-[90%]">
|
||||||
|
<button
|
||||||
|
class="w-4 h-4 flex justify-center align-middle text-gray-500 hover:text-blue-600 dark:text-gray-100 dark:hover:text-blue-600 shrink-0"
|
||||||
|
on:click="{() => {expanded = !expanded}}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
class:rotate-90="{expanded}"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<svg
|
||||||
|
class="w-2 h-2 flex shrink-0"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class:fill-green-500="{stream.status === 'online'}"
|
||||||
|
class:fill-yellow-500="{['blocked', 'timeout'].includes(stream.status)}"
|
||||||
|
class:fill-red-500="{stream.status === 'error'}"
|
||||||
|
>
|
||||||
|
<circle cx="50" cy="50" r="50" />
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
class="whitespace-nowrap text-sm text-gray-600 dark:text-gray-100 hover:text-blue-500 hover:underline inline-flex align-middle"
|
||||||
|
href="{stream.url}"
|
||||||
|
title="{stream.url}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span class="truncate max-w-[30rem]">{stream.url}</span
|
||||||
|
><span
|
||||||
|
class="inline-flex items-center pl-1 text-sm font-semibold text-gray-500 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
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>
|
||||||
|
</svg> </span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="flex shrink-0">
|
||||||
|
<CopyToClipboard text="{stream.url}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if expanded}
|
||||||
|
<div class="w-full flex px-2 py-4">
|
||||||
|
<JsonDataViewer data="{stream}" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
62
src/components/StreamsPopup.svelte
Normal file
62
src/components/StreamsPopup.svelte
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<script>
|
||||||
|
import StreamItem from './StreamItem.svelte'
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
const { close } = getContext('simple-modal')
|
||||||
|
|
||||||
|
export let streams = []
|
||||||
|
export let title = 'Streams'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative px-2 py-[10rem] flex justify-center" on:click|self="{close}">
|
||||||
|
<div class="relative bg-white rounded-md shadow dark:bg-gray-800 w-full max-w-2xl">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center py-4 pl-5 pr-4 rounded-t border-b dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<h3 class="text-l font-medium text-gray-800 dark:text-white inline-flex items-center">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center pr-2 text-sm font-semibold text-gray-500 rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
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"
|
||||||
|
></path>
|
||||||
|
</svg> </span
|
||||||
|
>{title}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
on:click="{close}"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-full text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-scroll overflow-x-hidden w-full">
|
||||||
|
<div class="p-6 space-y-2">
|
||||||
|
{#each streams as stream}
|
||||||
|
<StreamItem stream="{stream}" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,59 +1,41 @@
|
||||||
<script>
|
<script>
|
||||||
let scrollTop = 0
|
import '../app.css'
|
||||||
|
import NavBar from '../components/NavBar.svelte'
|
||||||
|
import Modal from 'svelte-simple-modal'
|
||||||
|
|
||||||
function scrollToTop(argument) {
|
let scrollTop = 0
|
||||||
document.body.scrollTop = 0
|
|
||||||
document.documentElement.scrollTop = 0
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:scrollY="{scrollTop}" />
|
<svelte:window bind:scrollY="{scrollTop}" />
|
||||||
|
<svelte:head>
|
||||||
|
<script>
|
||||||
|
if (document) {
|
||||||
|
let mode = localStorage.theme || 'light'
|
||||||
|
if (mode === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
localStorage.theme = 'dark'
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
localStorage.theme = 'light'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="navbar" style="background-color: transparent">
|
<header
|
||||||
<div class="navbar-end">
|
class:absolute="{scrollTop <= 150}"
|
||||||
<div class="navbar-item">
|
class:fixed="{scrollTop > 150}"
|
||||||
<a href="https://github.com/iptv-org/api">
|
class="z-40 w-full min-w-[360px]"
|
||||||
<span class="icon">
|
style="top: {scrollTop > 150 && scrollTop <= 210 ? scrollTop-210: 0}px"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<path
|
|
||||||
d="M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<slot></slot>
|
|
||||||
|
|
||||||
<footer
|
|
||||||
class="footer"
|
|
||||||
style="
|
|
||||||
background-color: transparent;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 3rem 1.5rem;
|
|
||||||
pointer-events: none;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div class="content">
|
<NavBar withSearch="{scrollTop > 150}" />
|
||||||
<div class="level">
|
</header>
|
||||||
<div class="level-left"></div>
|
|
||||||
<div class="level-right">
|
<main class="bg-slate-50 dark:bg-[#1d232e] min-h-screen pt-10 min-w-[360px]">
|
||||||
{#if scrollTop > 100}
|
<Modal
|
||||||
<button
|
unstyled="{true}"
|
||||||
class="button level-item is-hidden-mobile"
|
classBg="fixed top-0 left-0 z-40 w-screen h-screen flex flex-col bg-black/[.7] overflow-y-scroll"
|
||||||
on:click="{scrollToTop}"
|
closeButton="{false}"
|
||||||
style="pointer-events: auto"
|
><slot
|
||||||
>
|
/></Modal>
|
||||||
<span class="icon is-small">
|
</main>
|
||||||
<ion-icon name="arrow-up-outline"></ion-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
|
@ -1,122 +1,81 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import InfiniteLoading from 'svelte-infinite-loading'
|
||||||
|
import { fetchChannels, hasQuery, countries, filteredChannels, query, search } from '../store.js'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import CountryItem from '../components/CountryItem.svelte'
|
import CountryItem from '../components/CountryItem.svelte'
|
||||||
|
import SearchField from '../components/SearchField.svelte'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
let query = ''
|
let _countries = []
|
||||||
let normQuery = ''
|
const initLimit = 10
|
||||||
let regQuery = ''
|
let limit = initLimit
|
||||||
|
let infiniteId = +new Date()
|
||||||
let isLoading = true
|
let isLoading = true
|
||||||
let countries = []
|
|
||||||
let channels = []
|
|
||||||
let filtered = []
|
|
||||||
|
|
||||||
$: grouped = _.groupBy(filtered, 'country')
|
const unsubscribe = filteredChannels.subscribe(reset)
|
||||||
|
onDestroy(unsubscribe)
|
||||||
|
|
||||||
function search() {
|
$: visible = _countries.slice(0, limit)
|
||||||
normQuery = query.replace(/\s/g, '').toLowerCase()
|
|
||||||
regQuery = new RegExp(query)
|
|
||||||
|
|
||||||
if (!normQuery) {
|
$: grouped = _.groupBy($filteredChannels, 'country.code')
|
||||||
filtered = channels
|
|
||||||
|
function reset() {
|
||||||
|
limit = initLimit
|
||||||
|
infiniteId = +new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore({ detail: { loaded, complete } }) {
|
||||||
|
if (limit < _countries.length) {
|
||||||
|
limit++
|
||||||
|
loaded()
|
||||||
|
} else {
|
||||||
|
complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered = channels.filter(c => {
|
|
||||||
const normResult = c.key.includes(normQuery)
|
|
||||||
const regResult = regQuery ? regQuery.test(c.name) || regQuery.test(c.id) : false
|
|
||||||
|
|
||||||
return normResult || regResult
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let guides = await fetch('https://iptv-org.github.io/api/guides.json')
|
const params = new URLSearchParams(window.location.search)
|
||||||
.then(response => response.json())
|
const q = params.get('q')
|
||||||
.catch(console.log)
|
if (q) {
|
||||||
guides = guides.length ? guides : []
|
query.set(q)
|
||||||
guides = _.groupBy(guides, 'channel')
|
hasQuery.set(true)
|
||||||
|
}
|
||||||
channels = await fetch('https://iptv-org.github.io/api/channels.json')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(arr =>
|
|
||||||
arr.map(c => {
|
|
||||||
c.key = `${c.id}_${c.name}`.replace(/\s/g, '').toLowerCase()
|
|
||||||
c.guides = guides[c.id] || []
|
|
||||||
return c
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(err => {
|
|
||||||
console.log(err)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const countriesJson = await fetch('https://iptv-org.github.io/api/countries.json')
|
|
||||||
.then(response => response.json())
|
|
||||||
.catch(console.log)
|
|
||||||
|
|
||||||
countries = countriesJson.map(i => {
|
|
||||||
i.expanded = false
|
|
||||||
return i
|
|
||||||
})
|
|
||||||
|
|
||||||
filtered = channels
|
|
||||||
|
|
||||||
|
await fetchChannels()
|
||||||
|
_countries = Object.values($countries)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
|
if ($hasQuery) {
|
||||||
|
search($query)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="section">
|
<svelte:head>
|
||||||
<div class="container">
|
<title>iptv-org</title>
|
||||||
<div class="columns is-centered">
|
<meta name="description" content="Collection of resources dedicated to IPTV" />
|
||||||
<div class="column is-9">
|
</svelte:head>
|
||||||
<form class="mb-5" on:submit|preventDefault="{search}">
|
|
||||||
<div class="field-body">
|
|
||||||
<div class="field is-expanded">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="search"
|
|
||||||
bind:value="{query}"
|
|
||||||
placeholder="Search by channel name..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-info" type="submit">
|
|
||||||
<span class="icon is-small is-right">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style="width: 1.25rem; height: 1.25rem"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#ffffff"
|
|
||||||
d="M456.69 421.39L362.6 327.3a173.81 173.81 0 0034.84-104.58C397.44 126.38 319.06 48 222.72 48S48 126.38 48 222.72s78.38 174.72 174.72 174.72A173.81 173.81 0 00327.3 362.6l94.09 94.09a25 25 0 0035.3-35.3zM97.92 222.72a124.8 124.8 0 11124.8 124.8 124.95 124.95 0 01-124.8-124.8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !isLoading}
|
|
||||||
<p class="help">Found { filtered.length.toLocaleString() } channels</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{#if isLoading}
|
<section class="container max-w-5xl mx-auto px-2 py-20">
|
||||||
<div class="level">
|
<SearchField bind:isLoading="{isLoading}" bind:found="{$filteredChannels.length}"></SearchField>
|
||||||
<div class="level-item">Loading...</div>
|
{#if isLoading}
|
||||||
</div>
|
<div
|
||||||
{/if} {#each countries as country}
|
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
|
>
|
||||||
bind:country="{country}"
|
loading...
|
||||||
bind:channels="{grouped[country.code]}"
|
|
||||||
bind:normQuery="{normQuery}"
|
|
||||||
></CountryItem>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if} {#each visible as country} {#if grouped[country.code] && grouped[country.code].length > 0}
|
||||||
|
<CountryItem
|
||||||
|
bind:country="{country}"
|
||||||
|
bind:channels="{grouped[country.code]}"
|
||||||
|
bind:hasQuery="{$hasQuery}"
|
||||||
|
></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>
|
||||||
|
|
191
src/store.js
Normal file
191
src/store.js
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import { writable, get } from 'svelte/store'
|
||||||
|
import { transliterate } from 'transliteration'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export const query = writable('')
|
||||||
|
export const hasQuery = writable(false)
|
||||||
|
export const channels = writable([])
|
||||||
|
export const countries = writable({})
|
||||||
|
export const filteredChannels = writable([])
|
||||||
|
|
||||||
|
export function search(_query) {
|
||||||
|
setSearchParam('q', _query)
|
||||||
|
const parts = _query.toLowerCase().match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g) || []
|
||||||
|
const filters = []
|
||||||
|
for (let value of parts) {
|
||||||
|
let field = '_key'
|
||||||
|
if (value.includes(':')) {
|
||||||
|
;[field, value] = value.split(':')
|
||||||
|
value = value.replace(/\"/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field && value) {
|
||||||
|
filters.push({ field, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filters.length) {
|
||||||
|
hasQuery.set(false)
|
||||||
|
filteredChannels.set(get(channels))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = get(channels).filter(c => {
|
||||||
|
for (let f of filters) {
|
||||||
|
if (!c._searchable[f.field] || c._searchable[f.field].indexOf(f.value) === -1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
filteredChannels.set(filtered)
|
||||||
|
|
||||||
|
hasQuery.set(true)
|
||||||
|
|
||||||
|
console.log('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchChannels() {
|
||||||
|
let _countries = await fetch('https://iptv-org.github.io/api/countries.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data =>
|
||||||
|
data.map(i => {
|
||||||
|
i.expanded = false
|
||||||
|
return i
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(data => _.keyBy(data, 'code'))
|
||||||
|
.catch(console.error)
|
||||||
|
countries.set(_countries)
|
||||||
|
|
||||||
|
let _regions = await fetch('https://iptv-org.github.io/api/regions.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.keyBy(data, 'code'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _subdivisions = await fetch('https://iptv-org.github.io/api/subdivisions.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.keyBy(data, 'code'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _languages = await fetch('https://iptv-org.github.io/api/languages.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.keyBy(data, 'code'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _categories = await fetch('https://iptv-org.github.io/api/categories.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.keyBy(data, 'id'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _streams = await fetch('https://iptv-org.github.io/api/streams.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.groupBy(data, 'channel'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _guides = await fetch('https://iptv-org.github.io/api/guides.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => (data.length ? data : []))
|
||||||
|
.then(data => _.groupBy(data, 'channel'))
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
let _channels = await fetch('https://iptv-org.github.io/api/channels.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(arr =>
|
||||||
|
arr.map(c => {
|
||||||
|
c._raw = JSON.parse(JSON.stringify(c))
|
||||||
|
c._streams = _streams[c.id] || []
|
||||||
|
c._guides = _guides[c.id] || []
|
||||||
|
|
||||||
|
for (let field in c) {
|
||||||
|
switch (field) {
|
||||||
|
case 'languages':
|
||||||
|
c.languages = c.languages.map(code => _languages[code]).filter(i => i)
|
||||||
|
break
|
||||||
|
case 'broadcast_area':
|
||||||
|
c.broadcast_area = c.broadcast_area
|
||||||
|
.map(value => {
|
||||||
|
const [type, code] = value.split('/')
|
||||||
|
switch (type) {
|
||||||
|
case 'c':
|
||||||
|
return _countries[code]
|
||||||
|
case 'r':
|
||||||
|
return _regions[code]
|
||||||
|
case 's':
|
||||||
|
return _subdivisions[code]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(i => i)
|
||||||
|
break
|
||||||
|
case 'categories':
|
||||||
|
c.categories = c.categories.map(id => _categories[id]).filter(i => i)
|
||||||
|
break
|
||||||
|
case 'country':
|
||||||
|
c.country = _countries[c.country]
|
||||||
|
break
|
||||||
|
case 'subdivision':
|
||||||
|
c.subdivision = _subdivisions[c.subdivision]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c._searchable = generateSearchable(c)
|
||||||
|
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
channels.set(_channels)
|
||||||
|
filteredChannels.set(_channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSearchKey(c) {
|
||||||
|
const translit = c.native_name ? transliterate(c.native_name) : null
|
||||||
|
|
||||||
|
return [c.id, c.name, c.native_name, translit]
|
||||||
|
.map(v => v || '')
|
||||||
|
.filter(v => v)
|
||||||
|
.join('_')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSearchable(c) {
|
||||||
|
const searchable = {}
|
||||||
|
for (let key in c) {
|
||||||
|
if (key.startsWith('_') || c[key] === null || c[key] === undefined) continue
|
||||||
|
if (Array.isArray(c[key])) {
|
||||||
|
searchable[key] = c[key]
|
||||||
|
.map(v => (v.name ? v.name.toLowerCase() : null))
|
||||||
|
.filter(v => v)
|
||||||
|
.join(',')
|
||||||
|
} else if (typeof c[key] === 'object' && c[key].name) {
|
||||||
|
searchable[key] = c[key].name.toLowerCase()
|
||||||
|
} else {
|
||||||
|
searchable[key] = c[key].toString().toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchable._key = generateSearchKey(c)
|
||||||
|
|
||||||
|
return searchable
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchParam(key, value) {
|
||||||
|
if (window.history.pushState) {
|
||||||
|
let query = key && value ? `?${key}=${value}` : ''
|
||||||
|
query = query.replace(/\+/g, '%2B')
|
||||||
|
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}${query}`
|
||||||
|
window.history.pushState({ path: url }, '', url)
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
|
@ -5,8 +5,12 @@ const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: 'docs',
|
pages: 'docs',
|
||||||
assets: 'docs'
|
assets: 'docs',
|
||||||
})
|
precompress: true
|
||||||
|
}),
|
||||||
|
prerender: {
|
||||||
|
default: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue