Merge pull request #471 from 4gray/v1.0.0-beta.3

v1.0.0-beta.3
This commit is contained in:
4gray authored and GitHub committed 2024-11-28 23:59:58 +01:00
commit c7037dfe64
41 files changed
+1452 -657

No files matched your search

+20 -52
View File
@@ -1,12 +1,12 @@
{
"name": "iptvnator",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "iptvnator",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"dependencies": {
"@angular-builders/jest": "17.0.0",
"@angular/animations": "18.2.1",
@@ -16,6 +16,7 @@
"@ngrx/effects": "18.0.2",
"@ngrx/entity": "18.0.2",
"@ngrx/router-store": "18.0.2",
"@ngrx/signals": "18.1.1",
"@ngrx/store": "18.0.2",
"@ngrx/store-devtools": "18.0.2",
"@ngx-pwa/local-storage": "18.0.0",
@@ -39,7 +40,6 @@
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
"lodash": "4.17.21",
"moment": "2.30.1",
"nedb-promises": "6.0.3",
"ngx-indexed-db": "19.0.0",
"ngx-skeleton-loader": "9.0.0",
"ngx-whats-new": "0.4.0",
@@ -5656,6 +5656,23 @@
"rxjs": "^6.5.3 || ^7.5.0"
}
},
"node_modules/@ngrx/signals": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-18.1.1.tgz",
"integrity": "sha512-7AaFp4VrzYhnDHlF15VZAMvp067zIC5arRRRMV+i+oi95sQVDXezpj74KTIguwRbZ3FU+GKGzFCNrwgYvginSg==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "^18.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
},
"peerDependenciesMeta": {
"rxjs": {
"optional": true
}
}
},
"node_modules/@ngrx/store": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-18.0.2.tgz",
@@ -6615,26 +6632,6 @@
}
}
},
"node_modules/@seald-io/binary-search-tree": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz",
"integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA=="
},
"node_modules/@seald-io/nedb": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-2.2.2.tgz",
"integrity": "sha512-z91J3pem4ENzHuu9BilOSdlGL2S14OQYePPdvBcPHgHr+s51VIUwQARcOjx21KvtkA27vEpgPqzrVKh7nSlIfw==",
"dependencies": {
"@seald-io/binary-search-tree": "^1.0.2",
"async": "0.2.10",
"localforage": "^1.9.0"
}
},
"node_modules/@seald-io/nedb/node_modules/async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -14843,11 +14840,6 @@
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@@ -17897,14 +17889,6 @@
}
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -18143,14 +18127,6 @@
"node": ">= 12.13.0"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -19290,14 +19266,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/nedb-promises": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-6.0.3.tgz",
"integrity": "sha512-pd0N6EsIPhXsfqdLcBJGig1FoTdV8wtuiXB7rBUqycJdUSA6wdI5BlkwEAjDBWIwZc4+xqBHRLtuAB7/bjS+rg==",
"dependencies": {
"@seald-io/nedb": "^2.2.0"
}
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "iptvnator",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"description": "IPTV player application.",
"homepage": "https://github.com/4gray/iptvnator",
"author": {
@@ -43,6 +43,7 @@
"@ngrx/effects": "18.0.2",
"@ngrx/entity": "18.0.2",
"@ngrx/router-store": "18.0.2",
"@ngrx/signals": "18.1.1",
"@ngrx/store": "18.0.2",
"@ngrx/store-devtools": "18.0.2",
"@ngx-pwa/local-storage": "18.0.0",
+373 -36
View File
@@ -4,10 +4,14 @@ version = 3
[[package]]
name = "IPTVnator"
version = "1.0.0-beta.2"
version = "1.0.0-beta.3"
dependencies = [
"chrono",
"fix-path-env",
"flate2",
"log",
"quick-xml 0.31.0",
"reqwest 0.11.27",
"serde",
"serde_json",
"tauri",
@@ -21,6 +25,8 @@ dependencies = [
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tokio",
"xmltv",
]
[[package]]
@@ -159,6 +165,19 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-compression"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -528,8 +547,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
@@ -544,7 +565,7 @@ dependencies = [
"cocoa-foundation",
"core-foundation 0.10.0",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"libc",
"objc",
]
@@ -651,7 +672,7 @@ dependencies = [
"bitflags 2.6.0",
"core-foundation 0.10.0",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@@ -954,7 +975,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
"winreg",
"winreg 0.52.0",
]
[[package]]
@@ -1137,6 +1158,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1144,7 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@@ -1158,6 +1188,12 @@ dependencies = [
"syn 2.0.86",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -1565,6 +1601,25 @@ dependencies = [
"syn 2.0.86",
]
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.6.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.6"
@@ -1576,7 +1631,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http",
"http 1.1.0",
"indexmap 2.6.0",
"slab",
"tokio",
@@ -1646,6 +1701,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.11",
]
[[package]]
name = "http"
version = "1.1.0"
@@ -1657,6 +1723,17 @@ dependencies = [
"itoa 1.0.11",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -1664,7 +1741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
"http 1.1.0",
]
[[package]]
@@ -1675,8 +1752,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"http 1.1.0",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -1686,6 +1763,36 @@ version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa 1.0.11",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.5.0"
@@ -1695,9 +1802,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"httparse",
"itoa 1.0.11",
"pin-project-lite",
@@ -1713,8 +1820,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http",
"hyper",
"http 1.1.0",
"hyper 1.5.0",
"hyper-util",
"rustls",
"rustls-pki-types",
@@ -1724,6 +1831,19 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.31",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
@@ -1733,9 +1853,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.5.0",
"pin-project-lite",
"socket2",
"tokio",
@@ -2188,6 +2308,23 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2273,7 +2410,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.86",
@@ -2542,6 +2679,50 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.86",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2947,6 +3128,16 @@ dependencies = [
"psl-types",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quick-xml"
version = "0.32.0"
@@ -3174,6 +3365,48 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"async-compression",
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.31",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
]
[[package]]
name = "reqwest"
version = "0.12.9"
@@ -3187,11 +3420,11 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper 1.5.0",
"hyper-rustls",
"hyper-util",
"ipnet",
@@ -3203,13 +3436,13 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"sync_wrapper 1.0.1",
"system-configuration 0.6.1",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3354,6 +3587,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -3395,6 +3637,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.8.21"
@@ -3440,6 +3691,29 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.22.0"
@@ -3705,7 +3979,7 @@ dependencies = [
"bytemuck",
"cfg_aliases 0.2.1",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2",
@@ -3854,6 +4128,12 @@ dependencies = [
"syn 2.0.86",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.1"
@@ -3872,6 +4152,17 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -3880,7 +4171,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"system-configuration-sys",
"system-configuration-sys 0.6.0",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
@@ -3995,7 +4296,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http",
"http 1.1.0",
"jni",
"libc",
"log",
@@ -4007,7 +4308,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.9",
"serde",
"serde_json",
"serde_repr",
@@ -4155,9 +4456,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96"
dependencies = [
"data-url",
"http",
"http 1.1.0",
"regex",
"reqwest",
"reqwest 0.12.9",
"schemars",
"serde",
"serde_json",
@@ -4251,11 +4552,11 @@ dependencies = [
"dirs",
"flate2",
"futures-util",
"http",
"http 1.1.0",
"infer",
"minisign-verify",
"percent-encoding",
"reqwest",
"reqwest 0.12.9",
"semver",
"serde",
"serde_json",
@@ -4294,7 +4595,7 @@ checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784"
dependencies = [
"dpi",
"gtk",
"http",
"http 1.1.0",
"jni",
"raw-window-handle",
"serde",
@@ -4312,7 +4613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
dependencies = [
"gtk",
"http",
"http 1.1.0",
"jni",
"log",
"objc2",
@@ -4485,6 +4786,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -4504,6 +4806,16 @@ dependencies = [
"syn 2.0.86",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
@@ -4817,6 +5129,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -5509,6 +5827,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.52.0"
@@ -5533,7 +5861,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http",
"http 1.1.0",
"javascriptcore-rs",
"jni",
"kuchikiki",
@@ -5611,6 +5939,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "xmltv"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe9c95973cc08e1ebbc63705f61643d63a72614dd42d4c33f0e789da5c96b3d"
dependencies = [
"serde",
]
[[package]]
name = "zbus"
version = "5.1.1"
+8 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "IPTVnator"
version = "1.0.0-beta.2"
version = "1.0.0-beta.3"
description = "IPTVnator"
authors = ["4gray"]
license = "MIT"
@@ -21,7 +21,7 @@ tauri-build = { version = "2.0.1", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.0.6", features = [] }
tauri = { version = "2.0.6", features = ["macos-private-api"] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-http = {version = "2", features = ["unsafe-headers"] }
tauri-plugin-shell = "2"
@@ -30,6 +30,12 @@ tauri-plugin-os = "2"
tauri-plugin-fs = "2"
tauri-plugin-process = "2"
tauri-plugin-dialog = "2"
xmltv = "0.9"
tokio = { version = "1.0", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.11", features = ["json", "gzip"] }
flate2 = "1.0"
quick-xml = { version = "0.31", features = ["serialize"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"
+2 -1
View File
@@ -39,6 +39,7 @@
"fs:default",
"updater:default",
"dialog:default",
"process:default"
"process:default",
"core:window:allow-set-fullscreen"
]
}
+235
View File
@@ -0,0 +1,235 @@
use chrono::{DateTime, Utc};
use flate2::read::GzDecoder;
use log::{info, warn};
use quick_xml::de::from_str;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::sync::Mutex;
use tokio::sync::OnceCell;
use xmltv::Tv;
// Modified to store multiple EPG sources
#[derive(Debug)]
struct EpgStore {
sources: Vec<EpgSource>,
}
#[derive(Debug)]
struct EpgSource {
url: String,
programs: HashMap<String, Vec<Program>>,
}
// Replace the static EPG_DATA with the new structure
static EPG_DATA: OnceCell<Mutex<Option<EpgStore>>> = OnceCell::const_new();
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Program {
pub start: DateTime<Utc>,
pub stop: DateTime<Utc>,
pub title: String,
pub desc: Option<String>,
pub channel: String,
pub category: Option<String>,
pub icon: Option<String>,
pub language: Option<String>,
}
fn parse_xmltv_datetime(datetime: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_str(datetime, "%Y%m%d%H%M%S %z")
.ok()
.map(|dt| dt.with_timezone(&Utc))
}
pub async fn fetch_and_parse_epg(url: String) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting EPG fetch from URL: {}", url);
let new_source = fetch_single_epg_source(url.clone()).await?;
// Update EPG store
match EPG_DATA.get() {
Some(epg_data) => {
let mut guard = epg_data.lock().unwrap();
let store = guard.get_or_insert_with(|| EpgStore {
sources: Vec::new(),
});
// Remove existing source with same URL if exists
store.sources.retain(|source| source.url != url);
// Add new source
store.sources.push(new_source);
info!("Updated EPG store for URL: {}", url);
}
None => {
EPG_DATA
.set(Mutex::new(Some(EpgStore {
sources: vec![new_source],
})))
.unwrap();
info!("Created new EPG data store");
}
}
Ok(())
}
// New helper function to fetch and parse a single EPG source
async fn fetch_single_epg_source(url: String) -> Result<EpgSource, Box<dyn std::error::Error>> {
info!("Starting EPG fetch from URL: {}", url);
// Fetch content
let client = reqwest::Client::new();
let content = if url.ends_with(".gz") {
info!("Fetching and decompressing gzipped EPG data...");
let bytes = client.get(&url).send().await?.bytes().await?;
let mut decoder = GzDecoder::new(&bytes[..]);
let mut string = String::new();
decoder.read_to_string(&mut string)?;
info!("Successfully decompressed EPG data");
string
} else {
info!("Fetching EPG data...");
client.get(&url).send().await?.text().await?
};
info!("Parsing EPG XML data...");
let tv: Tv = from_str(&content)?;
// Get channel name mapping
let channel_names: HashMap<String, String> = tv
.channels
.into_iter()
.filter_map(|channel| {
channel
.display_names
.first()
.map(|name| (channel.id, name.name.clone()))
})
.collect();
info!("Converting EPG programs...");
let programs: Vec<Program> = tv
.programmes
.into_iter()
.filter_map(|p| {
let channel_name = match channel_names.get(&p.channel) {
Some(name) => name.clone(),
None => {
warn!("No channel name found for id: {}", p.channel);
return None;
}
};
let parse_datetime = |opt_str: &Option<String>| -> Option<DateTime<Utc>> {
opt_str.as_deref().and_then(parse_xmltv_datetime)
};
let start = match parse_datetime(&Some(p.start)) {
Some(dt) => dt,
None => {
warn!(
"Invalid start time format for program in channel {}",
p.channel
);
return None;
}
};
let stop = match parse_datetime(&p.stop) {
Some(dt) => dt,
None => {
warn!(
"Invalid stop time format for program in channel {}",
p.channel
);
return None;
}
};
let title = match p.titles.first() {
Some(t) => t.value.clone(),
None => {
warn!("Program without title found in channel {}", p.channel);
return None;
}
};
Some(Program {
start,
stop,
title,
desc: p.descriptions.first().map(|d| d.value.clone()),
channel: channel_name,
category: p.categories.first().map(|c| c.name.clone()),
icon: p.icons.first().map(|i| i.src.clone()),
language: p.language.map(|lang| lang.value),
})
})
.collect();
info!("Successfully parsed {} EPG programs", programs.len());
// Group by channel
let program_map = programs
.into_iter()
.fold(HashMap::new(), |mut acc, program| {
acc.entry(program.channel.clone())
.or_insert_with(Vec::new)
.push(program);
acc
});
info!("EPG data grouped into {} channels", program_map.len());
Ok(EpgSource {
url,
programs: program_map,
})
}
pub async fn get_programs_by_channel(
channel_name: String,
) -> Result<Vec<Program>, Box<dyn std::error::Error>> {
info!("Retrieving programs for channel name: {}", channel_name);
let programs = match EPG_DATA.get() {
Some(data) => {
let guard = data.lock().unwrap();
match &*guard {
Some(store) => {
// Search through all sources and combine matches
let mut all_programs = Vec::new();
for source in &store.sources {
if let Some(programs) = source.programs.get(&channel_name) {
info!(
"Found {} programs for channel {} in source {}",
programs.len(),
channel_name,
source.url
);
all_programs.extend(programs.clone());
}
}
// Sort programs by start time
all_programs.sort_by(|a, b| a.start.cmp(&b.start));
all_programs
}
None => Vec::new(),
}
}
None => Vec::new(),
};
if programs.is_empty() {
warn!("No programs found for channel name: {}", channel_name);
} else {
info!(
"Found {} total programs for channel {}",
programs.len(),
channel_name
);
}
Ok(programs)
}
+26 -1
View File
@@ -78,6 +78,26 @@ async fn open_in_vlc(url: String, path: String) -> Result<(), String> {
Ok(())
}
mod epg;
use epg::get_programs_by_channel;
#[tauri::command]
async fn fetch_epg(url: Vec<String>) -> Result<(), String> {
for single_url in url {
epg::fetch_and_parse_epg(single_url)
.await
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
async fn get_channel_programs(channel_id: String) -> Result<Vec<epg::Program>, String> {
get_programs_by_channel(channel_id)
.await
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -89,7 +109,12 @@ pub fn run() {
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.invoke_handler(tauri::generate_handler![open_in_mpv, open_in_vlc])
.invoke_handler(tauri::generate_handler![
fetch_epg,
get_channel_programs,
open_in_mpv,
open_in_vlc
])
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
+2 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "iptvnator",
"version": "1.0.0-2",
"version": "1.0.0-3",
"identifier": "com.4gray.dev",
"build": {
"frontendDist": "../dist",
@@ -28,6 +28,7 @@
"titleBarStyle": "Transparent"
}
],
"macOSPrivateApi": true,
"security": {
"csp": {
"default-src": [
-4
View File
@@ -12,8 +12,6 @@ import * as semver from 'semver';
import { IpcCommand } from '../../shared/ipc-command.class';
import {
AUTO_UPDATE_PLAYLISTS,
EPG_ERROR,
EPG_FETCH_DONE,
ERROR,
OPEN_FILE,
SETTINGS_UPDATE,
@@ -50,8 +48,6 @@ export class AppComponent {
commandsList = [
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
new IpcCommand(EPG_FETCH_DONE, () => this.epgService.onEpgFetchDone()),
new IpcCommand(EPG_ERROR, () => this.epgService.onEpgError()),
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
this.showErrorAsNotification(response)
+2 -5
View File
@@ -48,17 +48,14 @@ function isTauri() {
/**
* Conditionally imports the necessary service based on the current environment
*/
export function DataFactory(
ngxIndexedDBService: NgxIndexedDBService,
http: HttpClient
) {
export function DataFactory() {
if (isElectron()) {
return new ElectronService();
}
if (isTauri()) {
return new TauriService();
}
return new PwaService(http);
return new PwaService();
}
@NgModule({
@@ -13,6 +13,8 @@
matInput
class="full-width"
type="search"
spellcheck="false"
autocomplete="off"
#search
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
[(ngModel)]="searchTerm.name"
@@ -55,100 +57,105 @@
</ng-template>
<mat-nav-list id="groups-list">
<mat-accordion multi>
<ng-container
*ngFor="let groups of groupedChannels | keyvalue"
>
<mat-expansion-panel>
<mat-expansion-panel-header
*ngIf="groups.value.length > 0"
>
{{
groups.key ||
('CHANNELS.UNGROUPED' | translate)
| titlecase
}}
({{ groups.value.length }})
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container
*ngFor="
let channel of groups.value;
index as i;
trackBy: trackByFn
"
>
<app-channel-list-item
[name]="
i +
1 +
'. ' +
(channel?.name ||
'CHANNELS.UNNAMED_CHANNEL'
| translate)
@for (groups of groupedChannels | keyvalue; track $index) {
@if (groups.value.length > 0) {
<mat-expansion-panel>
<mat-expansion-panel-header>
{{
groups.key ||
('CHANNELS.UNGROUPED' | translate)
| titlecase
}}
({{ groups.value.length }})
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container
*ngFor="
let channel of groups.value;
index as i;
trackBy: trackByFn
"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="selected?.id === channel.id"
></app-channel-list-item>
</ng-container>
</ng-template>
</mat-expansion-panel>
</ng-container>
>
<app-channel-list-item
[name]="
i +
1 +
'. ' +
(channel?.name ||
'CHANNELS.UNNAMED_CHANNEL'
| translate)
"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="
selected?.id === channel.id
"
></app-channel-list-item>
</ng-container>
</ng-template>
</mat-expansion-panel>
}
}
</mat-accordion>
</mat-nav-list>
</mat-tab>
<mat-tab *ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'">
<ng-template mat-tab-label>
<mat-icon class="tab-icon">star</mat-icon>
<div class="tab-title">
{{ 'CHANNELS.FAVORITES' | translate }}
</div>
</ng-template>
<mat-nav-list
cdkDropList
*ngIf="favorites$ | async as favorites"
(cdkDropListDropped)="drop($event, favorites)"
id="favorites-list"
>
<ng-container *ngIf="favorites.length > 0; else noFavorites">
<app-channel-list-item
*ngFor="
let channel of favorites;
index as i;
trackBy: trackByFn
"
[name]="
i +
1 +
'. ' +
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
| translate)
"
[isDraggable]="true"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="selected?.id === channel?.id"
[showFavoriteButton]="true"
(favoriteToggled)="
toggleFavoriteChannel(channel, $event)
"
></app-channel-list-item>
</ng-container>
<ng-template #noFavorites>
<mat-list-item
><strong>{{
'CHANNELS.NO_FAVORITES' | translate
}}</strong></mat-list-item
>
<mat-list-item>
{{
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
}}</mat-list-item
>
@let showFavorites = (playlistId$ | async) !== 'GLOBAL_FAVORITES';
@if (showFavorites) {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">star</mat-icon>
<div class="tab-title">
{{ 'CHANNELS.FAVORITES' | translate }}
</div>
</ng-template>
</mat-nav-list>
</mat-tab>
@let favorites = favorites$ | async;
@if (favorites) {
<mat-nav-list
cdkDropList
(cdkDropListDropped)="drop($event, favorites)"
id="favorites-list"
>
@if (favorites.length > 0) {
<app-channel-list-item
*ngFor="
let channel of favorites;
index as i;
trackBy: trackByFn
"
[name]="
i +
1 +
'. ' +
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
| translate)
"
[isDraggable]="true"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="selected?.id === channel?.id"
[showFavoriteButton]="true"
(favoriteToggled)="
toggleFavoriteChannel(channel, $event)
"
/>
} @else {
<mat-list-item
><strong>{{
'CHANNELS.NO_FAVORITES' | translate
}}</strong></mat-list-item
>
<mat-list-item>
{{
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
}}</mat-list-item
>
}
</mat-nav-list>
}
</mat-tab>
}
</mat-tab-group>
} @else {
No channels there
@@ -1,4 +1,8 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import {
CdkDragDrop,
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { CommonModule, TitleCasePipe } from '@angular/common';
import {
@@ -22,6 +26,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import * as _ from 'lodash';
import { map, skipWhile } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { EpgService } from '../../../services/epg.service';
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
import * as PlaylistActions from '../../../state/actions';
import {
@@ -36,8 +41,11 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
templateUrl: './channel-list-container.component.html',
styleUrls: ['./channel-list-container.component.scss'],
imports: [
CommonModule,
ChannelListItemComponent,
CommonModule,
DragDropModule,
FilterPipe,
FormsModule,
MatDividerModule,
MatExpansionModule,
MatFormFieldModule,
@@ -45,11 +53,9 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
MatInputModule,
MatListModule,
MatTabsModule,
TranslateModule,
TitleCasePipe,
FilterPipe,
FormsModule,
ScrollingModule,
TitleCasePipe,
TranslateModule,
],
})
export class ChannelListContainerComponent {
@@ -116,7 +122,8 @@ export class ChannelListContainerComponent {
constructor(
private readonly store: Store,
private snackBar: MatSnackBar,
private translateService: TranslateService
private translateService: TranslateService,
private epgService: EpgService
) {}
/**
@@ -126,6 +133,12 @@ export class ChannelListContainerComponent {
selectChannel(channel: Channel): void {
this.selected = channel;
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
const epgChannelId = channel?.tvg?.id || channel?.name;
if (epgChannelId) {
this.epgService.getChannelPrograms(epgChannelId);
}
}
/**
@@ -6,7 +6,7 @@
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
</div>
<p data-test="title">{{ epgProgram.title[0].value }}</p>
<p data-test="title">{{ epgProgram.title }}</p>
<div class="subheading-2" *ngIf="epgProgram.title[0].lang">
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
</div>
@@ -16,13 +16,13 @@
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
</div>
<p data-test="category">{{ epgProgram.category[0].value }}</p>
<p data-test="category">{{ epgProgram.category }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.desc?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
</div>
<p data-test="desc">{{ epgProgram.desc[0].value }}</p>
<p data-test="desc">{{ epgProgram.desc }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.rating?.length > 0">
<div class="subheading-2">
@@ -17,7 +17,7 @@
>
}
@if (item.desc.length > 0) {
@if (item.desc?.length > 0) {
<span
(click)="$event.stopPropagation(); showDescription(item)"
matListItemMeta
@@ -1,118 +1,105 @@
@if (channel) {
<!-- channel info panel -->
<div id="channel-header" color="primary">
<div class="channel-info">
<div class="channel-icon">
@if (channel?.icon) {
<img [src]="channel.icon" width="48" />
}
</div>
<div class="channel-details">
<div class="channel-name">
{{ channel?.name[0]?.value }}
</div>
<div class="program-name">
{{
playingNow
? playingNow.title[0]?.value
: ('EPG.LIVE_STREAM' | translate)
}}
</div>
</div>
@let items = filteredItems$ | async;
@let timeshiftUntil = timeshiftUntil$ | async;
<!-- channel info panel -->
<div id="channel-header" color="primary">
<div class="channel-info">
<div class="channel-icon">
@if (channel?.icon) {
<img [src]="channel.icon" width="48" />
}
</div>
<mat-divider />
<div id="date-navigator">
<button
class="previous-day"
mat-icon-button
(click)="changeDate('prev')"
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
>
<mat-icon>chevron_left</mat-icon>
</button>
<span class="selected-date">
{{ dateToday | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}</span
>
<button
class="next-day"
mat-icon-button
(click)="changeDate('next')"
[matTooltip]="'EPG.NEXT_DAY' | translate"
>
<mat-icon>chevron_right</mat-icon>
</button>
<div class="channel-details">
<div class="channel-name">
{{ channel?.name }}
</div>
<div class="program-name">
{{
playingNow
? playingNow.title
: ('EPG.LIVE_STREAM' | translate)
}}
</div>
</div>
</div>
<mat-divider />
@let timeshiftUntil = timeshiftUntil$ | async;
@if (timeshiftUntil) {
<!-- program list -->
<mat-selection-list
id="program-list"
[multiple]="false"
hideSingleSelectionIndicator="true"
<div id="date-navigator">
<button
class="previous-day"
mat-icon-button
(click)="changeDate('prev')"
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
>
@if (items?.length > 0) {
<ng-container *ngFor="let item of items">
<mat-icon>chevron_left</mat-icon>
</button>
<span class="selected-date">
{{ dateToday | momentDate: 'YYYY-MM-DD' : 'MMMM Do, dddd' }}</span
>
<button
class="next-day"
mat-icon-button
(click)="changeDate('next')"
[matTooltip]="'EPG.NEXT_DAY' | translate"
>
<mat-icon>chevron_right</mat-icon>
</button>
</div>
</div>
<mat-divider />
@if (timeshiftUntil) {
<!-- program list -->
<mat-selection-list
id="program-list"
[multiple]="false"
hideSingleSelectionIndicator="true"
>
@if (items?.length > 0) {
@for (program of items; track program) {
@if (
program.start < timeshiftUntil || program.start >= timeNow
) {
<mat-list-option>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="program"
/>
</div>
<p matListItemLine>{{ program?.title }}</p>
</mat-list-option>
} @else {
<mat-list-option
*ngIf="
item.start < timeshiftUntil ||
item.start >= timeNow;
else timeshiftItem
[value]="program"
[class.active]="
timeNow >= program.start && timeNow <= program.stop
"
(click)="
setEpgProgram(
program,
timeNow >= program.start &&
timeNow <= program.stop,
program.start > timeshiftUntil
)
"
>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
[item]="program"
/>
</div>
<p matListItemLine>{{ item?.title[0]?.value }}</p>
<p matListItemLine [innerHTML]="program.title"></p>
</mat-list-option>
<ng-template #timeshiftItem>
<mat-list-option
[value]="item"
[class.active]="
timeNow >= item.start && timeNow <= item.stop
"
(click)="
setEpgProgram(
item,
timeNow >= item.start &&
timeNow <= item.stop,
item.start > timeshiftUntil
)
"
>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
/>
</div>
<p
matListItemLine
[innerHTML]="item?.title[0]?.value"
></p>
</mat-list-option>
</ng-template>
<mat-divider />
</ng-container>
} @else {
<p class="epg-not-available">
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
</p>
}
<mat-divider />
}
</mat-selection-list>
}
} @else {
<mat-list>
<mat-list-item>
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }}<br />
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION' | translate }}
</mat-list-item>
</mat-list>
} @else {
<p class="epg-not-available">
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
</p>
}
</mat-selection-list>
}
@@ -7,6 +7,7 @@
left: 0;
bottom: 0;
height: 100%;
overflow-x: hidden;
}
.active {
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
import { Component, NgZone } from '@angular/core';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
@@ -9,10 +9,11 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import moment from 'moment';
import { Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { EpgService } from '../../../services/epg.service';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import {
resetActiveEpgProgram,
@@ -29,8 +30,7 @@ export interface EpgData {
items: EpgProgram[];
}
const DATE_FORMAT = 'YYYYMMDD';
const DATE_TIME_FORMAT = 'YYYYMMDDHHmm ZZ';
const DATE_FORMAT = 'YYYY-MM-DD';
@Component({
standalone: true,
@@ -58,7 +58,7 @@ export class EpgListComponent {
dateToday: string;
/** Array with EPG programs */
items: EpgProgram[] = [];
items$ = this.epgService.currentEpgPrograms$;
/** Object with epg programs for the active channel */
programs: {
@@ -77,61 +77,65 @@ export class EpgListComponent {
/** Timeshift availability date, based on tvg-rec value from the channel */
timeshiftUntil$: Observable<string>;
/**
* Creates an instance of EpgListComponent
* @param store
* @param electronService
* @param ngZone
*/
private readonly selectedDate$ = new BehaviorSubject<string>(
moment().format(DATE_FORMAT)
);
/** Filtered EPG programs based on selected date */
filteredItems$ = combineLatest([this.items$, this.selectedDate$]).pipe(
map(([items, selectedDate]) =>
items
.filter(
(item) =>
moment(item.start).format('YYYY-MM-DD') === selectedDate
)
.sort((a, b) => moment(a.start).diff(moment(b.start)))
)
);
constructor(
private readonly store: Store,
private electronService: DataService,
private ngZone: NgZone
) {
this.electronService.listenOn(
EPG_GET_PROGRAM_DONE,
(event, response) => {
this.ngZone.run(() => this.handleEpgData(response));
}
);
}
private dataService: DataService,
private readonly epgService: EpgService
) {}
/**
* Subscribe for values from the store on component init
*/
ngOnInit(): void {
this.timeshiftUntil$ = this.store.select(selectActive).pipe(
// eslint-disable-next-line @ngrx/avoid-mapping-selectors
map((active) => {
console.log(active);
this.channel = {
id: active?.tvg?.id,
name: active?.name,
url: [active?.url],
icon: [active?.tvg?.logo],
};
return (
active?.tvg?.rec ||
active?.timeshift ||
active?.catchup?.days
);
}),
map((value) =>
moment(Date.now())
.subtract(value, 'days')
.format(DATE_TIME_FORMAT)
)
map((value) => moment().subtract(value, 'days').toISOString())
);
this.items$.subscribe((programs) => this.handleEpgData(programs));
this.dateToday = moment().format(DATE_FORMAT);
this.selectedDate$.next(this.dateToday);
}
/**
* Handles incoming epg programs for the active channel from the main process
* @param programs
*/
handleEpgData(programs: { payload: EpgData }): void {
if (programs?.payload?.items?.length > 0) {
this.programs = programs;
this.timeNow = moment(Date.now()).format(DATE_TIME_FORMAT);
this.dateToday = moment(Date.now()).format(DATE_FORMAT);
this.channel = programs.payload?.channel;
this.items = this.selectPrograms(programs);
handleEpgData(programs: EpgProgram[]): void {
this.timeNow = new Date().toISOString();
this.dateToday = moment().format(DATE_FORMAT);
if (programs.length > 0) {
this.setPlayingNow();
} else {
this.items = [];
this.channel = null;
this.store.dispatch(setCurrentEpgProgram(undefined));
}
@@ -139,23 +143,20 @@ export class EpgListComponent {
/**
* Selects the program based on the active date
* @param programs object with all available epg programs for the active channel
*/
selectPrograms(programs: { payload: EpgData }): EpgProgram[] {
const selectedDate = moment(this.dateToday).format('YYYY-MM-DD');
return programs.payload?.items
.filter((item) => item.start.includes(this.dateToday.toString()))
.filter(
(item) =>
moment(item.start).format('YYYY-MM-DD') === selectedDate
)
.map((program) => ({
...program,
start: moment(program.start, DATE_TIME_FORMAT).format(
DATE_TIME_FORMAT
),
stop: moment(program.stop, DATE_TIME_FORMAT).format(
DATE_TIME_FORMAT
),
start: program.start, // Keep ISO format
stop: program.stop, // Keep ISO format
}))
.sort((a, b) => {
return a.start.localeCompare(b.start);
});
.sort((a, b) => moment(a.start).diff(moment(b.start)));
}
/**
@@ -163,28 +164,37 @@ export class EpgListComponent {
* @param direction direction to switch
*/
changeDate(direction: 'next' | 'prev'): void {
let dateToSwitch;
if (direction === 'next') {
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
.add(1, 'days')
.format(DATE_FORMAT);
} else if (direction === 'prev') {
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
.subtract(1, 'days')
.format(DATE_FORMAT);
}
this.dateToday = dateToSwitch;
this.items = this.selectPrograms(this.programs);
const newDate = moment(this.selectedDate$.value)
[direction === 'next' ? 'add' : 'subtract'](1, 'days')
.format(DATE_FORMAT);
this.dateToday = newDate;
this.selectedDate$.next(newDate);
}
/**
* Sets the playing now variable based on the current time
*/
setPlayingNow(): void {
this.playingNow = this.items.find(
(item) => this.timeNow >= item.start && this.timeNow <= item.stop
);
this.store.dispatch(setCurrentEpgProgram({ program: this.playingNow }));
this.items$
.pipe(
map((items) =>
items.find((item) => {
const now = new Date().toISOString();
const start = new Date(item.start).toISOString();
const stop = new Date(item.stop).toISOString();
return now >= start && now <= stop;
})
)
)
.subscribe((playingNow) => {
this.playingNow = playingNow;
if (playingNow) {
this.store.dispatch(
setCurrentEpgProgram({ program: playingNow })
);
}
});
}
/**
@@ -211,6 +221,6 @@ export class EpgListComponent {
* Removes all ipc renderer listeners after destroy
*/
ngOnDestroy(): void {
this.electronService.removeAllListeners(EPG_GET_PROGRAM_DONE);
this.dataService.removeAllListeners(EPG_GET_PROGRAM_DONE);
}
}
@@ -75,32 +75,61 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
if (channel.url) {
const url = channel.url + (channel.epgParams ?? '');
const extension = getExtensionFromUrl(channel.url);
this.dataService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
userAgent: channel.http?.['user-agent'] ?? '',
referer: channel.http?.referrer ?? '',
});
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
console.log('... switching channel to ', channel.name, url);
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
console.error('something wrong with hls.js init...');
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
// Send IPC event and handle the response
this.dataService
.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
userAgent: channel.http?.['user-agent'] ?? '',
referer: channel.http?.referrer ?? '',
})
.then(() => {
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
console.log(
'... switching channel to ',
channel.name,
url
);
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
console.log('Using native video player...');
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
})
.catch((error) => {
console.error('Error setting user agent:', error);
// Continue playback even if setting user agent fails
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
});
}
}
@@ -6,17 +6,17 @@
>
<div class="channel-logo">
<img
[src]="epgProgram?.icon[0] || channel?.tvg?.logo"
[src]="epgProgram?.icon || channel?.tvg?.logo"
width="68"
onerror="this.style.display='none'"
/>
</div>
<div class="content">
<div class="program-name">
{{ epgProgram?.title[0]?.value }} | {{ channel.name }}
{{ epgProgram?.title }} | {{ channel.name }}
</div>
<div class="program-description">
{{ epgProgram?.desc[0]?.value }}
{{ epgProgram?.desc }}
</div>
<div class="program-progress">
<div
@@ -50,23 +50,23 @@
[attr.y]="barHeight * i + barHeight"
stroke="black"
></rect>
@if (item.icon[0]) {
@if (item.icon) {
<image
[attr.y]="barHeight * i + barHeight + 1"
[attr.href]="item.icon[0]"
[attr.href]="item.icon"
[attr.height]="barHeight - 2"
width="100"
[matTooltip]="item.name[0].value"
[matTooltip]="item.name"
></image>
}
@if (!item.icon[0]) {
@if (!item.icon) {
<foreignObject
[attr.y]="barHeight * i + barHeight"
width="100"
[attr.height]="barHeight"
>
<div class="channel-name">
{{ item.name[0].value }}
{{ item.name }}
</div>
</foreignObject>
}
@@ -99,7 +99,7 @@
<g *ngFor="let item of channels; let i = index">
<g
*ngFor="let program of item.programs; let a = index"
[matTooltip]="program.title[0].value"
[matTooltip]="program.title"
class="program-item"
>
<!-- program item -->
@@ -118,7 +118,7 @@
[attr.x]="program.startPosition"
>
<div
[innerHtml]="program.title[0]?.value"
[innerHtml]="program.title"
class="program-name"
></div>
</foreignObject>
@@ -28,7 +28,7 @@ rect {
#epg-container {
width: calc(100vw - 100px);
height: calc(100vh - 73px);
height: calc(100vh - 42px);
overflow-x: scroll;
overflow-y: hidden;
}
@@ -53,7 +53,6 @@ rect {
#epg-navigation {
display: flex;
background-color: white;
margin-top: 30px;
border-bottom: 2px solid #fff;
align-items: center;
}
@@ -7,23 +7,32 @@
>
<mat-icon>menu</mat-icon>
</button>
<button
*ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'"
mat-icon-button
(click)="addToFavorites(activeChannel)"
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
>
<mat-icon>
{{
(favorites$ | async).includes(activeChannel?.id)
? 'star'
: 'star_outline'
}}</mat-icon
@if ((playlistId$ | async) !== 'GLOBAL_FAVORITES') {
<button
mat-icon-button
(click)="addToFavorites(activeChannel)"
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
>
</button>
<mat-icon>
{{
(favorites$ | async).includes(activeChannel?.id)
? 'star'
: 'star_outline'
}}</mat-icon
>
</button>
}
{{ activeChannel?.name }}
<div class="spacer"></div>
<ng-container *ngIf="isEpgAvailable$ | async">
<button
mat-icon-button
(click)="openSettings()"
[matTooltip]="'MENU.SETTINGS' | translate"
>
<mat-icon>settings</mat-icon>
</button>
@let isEpgAvailable = isEpgAvailable$ | async;
@if (isEpgAvailable) {
<button
mat-icon-button
(click)="multiEpgClicked.emit()"
@@ -38,5 +47,5 @@
>
EPG
</button>
</ng-container>
}
</mat-toolbar>
@@ -1,6 +1,7 @@
import { AsyncPipe, CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
@@ -8,6 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Channel } from '../../../../../../shared/channel.interface';
import { SettingsComponent } from '../../../../settings/settings.component';
import { updateFavorites } from '../../../../state/actions';
import {
selectActivePlaylistId,
@@ -25,6 +27,7 @@ import {
MatToolbarModule,
MatTooltipModule,
TranslateModule,
MatDialogModule,
],
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
@@ -43,7 +46,8 @@ export class ToolbarComponent {
constructor(
private snackBar: MatSnackBar,
private store: Store,
private translateService: TranslateService
private translateService: TranslateService,
private dialog: MatDialog
) {}
/**
@@ -58,4 +62,12 @@ export class ToolbarComponent {
);
this.store.dispatch(updateFavorites({ channel }));
}
openSettings(): void {
this.dialog.open(SettingsComponent, {
width: '1000px',
height: '90%',
data: { isDialog: true },
});
}
}
@@ -90,6 +90,6 @@
</mat-drawer-content>
<!-- right sidebar content -->
<mat-drawer position="end" #drawerRight mode="side" disableClose>
<app-epg-list *ngIf="isElectron" />
<app-epg-list *ngIf="isTauri" />
</mat-drawer>
</mat-drawer-container>
@@ -8,6 +8,7 @@ import {
NgZone,
OnDestroy,
OnInit,
effect,
} from '@angular/core';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -25,6 +26,7 @@ import {
import { Playlist } from '../../../../../shared/playlist.interface';
import { DataService } from '../../../services/data.service';
import { PlaylistsService } from '../../../services/playlists.service';
import { SettingsStore } from '../../../services/settings-store.service';
import { Settings, VideoPlayer } from '../../../settings/settings.interface';
import { STORE_KEY } from '../../../shared/enums/store-keys.enum';
import * as PlaylistActions from '../../../state/actions';
@@ -118,7 +120,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
listeners = [];
isElectron = this.dataService.isElectron;
isTauri = this.dataService.getAppEnvironment() === 'tauri';
sidebarView: SidebarView = 'CHANNELS';
@@ -136,13 +138,22 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
private router: Router,
private snackBar: MatSnackBar,
private storage: StorageMap,
private store: Store
private store: Store,
private settingsStore: SettingsStore
) {
// Initialize volume from localStorage in constructor
const savedVolume = localStorage.getItem('volume');
if (savedVolume !== null) {
this.volume = Number(savedVolume);
}
// React to settings changes
effect(() => {
this.playerSettings = {
player: this.settingsStore.player(),
showCaptions: this.settingsStore.showCaptions(),
};
});
}
/**
@@ -210,7 +221,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
*/
setRendererListeners(): void {
this.commandsList.forEach((command) => {
if (this.isElectron) {
if (this.isTauri) {
this.dataService.listenOn(command.id, (event, response) =>
this.ngZone.run(() => command.execute(response))
);
@@ -243,7 +254,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
if (this.isElectron) {
if (this.isTauri) {
this.dataService.removeAllListeners(PLAYLIST_PARSE_RESPONSE);
} else {
this.listeners.forEach((listener) =>
+1 -1
View File
@@ -1,6 +1,6 @@
export interface EpgChannel {
id: string;
name: { lang: string; value: string }[];
name: string;
icon: string[];
url: string[];
}
@@ -15,7 +15,6 @@ import { EpgItem } from '../../xtream/epg-item.interface';
templateUrl: './epg-view.component.html',
imports: [
DatePipe,
EpgItemDescriptionComponent,
MatButtonModule,
MatDividerModule,
MatIconModule,
@@ -24,8 +23,8 @@ import { EpgItem } from '../../xtream/epg-item.interface';
],
styles: `
mat-list {
overflow-y: scroll;
height: calc(100vh - 459px);
/* overflow-y: scroll; */
height: 100vh;
}
`,
})
@@ -37,8 +36,8 @@ export class EpgViewComponent {
showDetails(item: EpgItem) {
this.dialog.open(EpgItemDescriptionComponent, {
data: {
title: [{ value: item.title, lang: item.lang }],
desc: [{ value: item.description }],
title: item.title ?? 'No title',
desc: item.description ?? 'No description',
},
});
}
@@ -7,12 +7,11 @@
.itv-container {
display: grid;
grid-template-columns: 330px 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
grid-template-rows: 50% 50%;
gap: 0 0;
grid-auto-flow: row;
grid-template-areas:
'sidebar video-player video-player'
'sidebar epg epg'
'sidebar epg epg';
height: 100%;
}
@@ -49,9 +48,10 @@
}
.epg-content {
height: 100%;
height: calc(50vh - 20px);
overflow-y: auto;
border-right: #111 1px solid;
padding-bottom: 20px;
}
.sidebar-content {
@@ -1,3 +1,7 @@
app-vjs-player > .video-js {
height: 100%;
height: 50vh !important;
}
app-html-video-player > #video-player {
height: 50vh !important;
}
+99 -40
View File
@@ -1,66 +1,125 @@
import { Injectable } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { EPG_FETCH } from '../../../shared/ipc-commands';
import { setEpgAvailableFlag } from '../state/actions';
import { DataService } from './data.service';
import { invoke } from '@tauri-apps/api/core';
import { BehaviorSubject, from } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { EpgProgram } from '../player/models/epg-program.model';
import * as PlaylistActions from '../state/actions';
@Injectable({
providedIn: 'root',
})
export class EpgService {
/** Default options for epg snackbar notifications */
epgSnackBarOptions: MatSnackBarConfig = {
verticalPosition: 'bottom',
horizontalPosition: 'start',
};
private epgAvailable = new BehaviorSubject<boolean>(false);
private currentEpgPrograms = new BehaviorSubject<EpgProgram[]>([]);
epgAvailable$ = this.epgAvailable.asObservable();
currentEpgPrograms$ = this.currentEpgPrograms.asObservable();
constructor(
private store: Store,
private electronService: DataService,
private snackBar: MatSnackBar,
private translate: TranslateService
private translate: TranslateService,
private store: Store
) {}
/**
* Fetches and updates EPG from the given sources
* @param urls epg source urls
* Fetches EPG from the given URLs
*/
fetchEpg(urls: string | string[]) {
const urlsArray = Array.isArray(urls) ? urls : [urls];
urlsArray.forEach((url) =>
this.electronService.sendIpcEvent(EPG_FETCH, {
url,
})
);
fetchEpg(urls: string[]): void {
this.showFetchSnackbar();
// Filter out empty URLs and send all URLs at once
const validUrls = urls.filter((url) => url?.trim());
if (validUrls.length === 0) return;
from(invoke('fetch_epg', { url: validUrls }))
.pipe(
tap(() => {
this.epgAvailable.next(true);
this.showSuccessSnackbar();
}),
catchError((err) => {
console.error('EPG fetch error:', err);
this.epgAvailable.next(false);
this.showErrorSnackbar();
throw err;
})
)
.subscribe();
}
showFetchSnackbar() {
/**
* Gets EPG programs for a specific channel
*/
getChannelPrograms(channelId: string): void {
console.log('Fetching EPG for channel ID:', channelId);
from(invoke<EpgProgram[]>('get_channel_programs', { channelId }))
.pipe(
tap((programs) => {
console.log('Received programs:', programs);
}),
map((programs) =>
programs.map((program) => ({
...program,
start: new Date(program.start).toISOString(),
stop: new Date(program.stop).toISOString(),
}))
),
catchError((err) => {
console.error('EPG get programs error:', err);
this.showErrorSnackbar();
this.currentEpgPrograms.next([]);
throw err;
})
)
.subscribe((programs) => {
if (programs.length === 0) {
this.store.dispatch(
PlaylistActions.setEpgAvailableFlag({ value: false })
);
} else {
this.store.dispatch(
PlaylistActions.setEpgAvailableFlag({ value: true })
);
}
this.currentEpgPrograms.next(programs);
console.log('Updated programs:', programs); // Debug log
});
}
/**
* Shows fetch in progress snackbar
*/
showFetchSnackbar(): void {
this.snackBar.open(this.translate.instant('EPG.FETCH_EPG'), null, {
duration: 2000,
horizontalPosition: 'start',
});
}
/**
* Shows success snackbar
*/
private showSuccessSnackbar(): void {
this.snackBar.open(this.translate.instant('EPG.FETCH_SUCCESS'), null, {
duration: 2000,
horizontalPosition: 'start',
});
}
/**
* Shows error snackbar
*/
private showErrorSnackbar(): void {
this.snackBar.open(
this.translate.instant('EPG.FETCH_EPG'),
this.translate.instant('EPG.ERROR'),
this.translate.instant('CLOSE'),
this.epgSnackBarOptions
);
}
onEpgFetchDone() {
this.store.dispatch(setEpgAvailableFlag({ value: true }));
this.snackBar.open(
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
null,
{
...this.epgSnackBarOptions,
duration: 2000,
horizontalPosition: 'start',
}
);
}
onEpgError() {
this.snackBar.open(this.translate.instant('EPG.ERROR'), null, {
...this.epgSnackBarOptions,
duration: 2000,
});
}
}
+25 -10
View File
@@ -143,20 +143,34 @@ export class PlaylistsService {
getPortalFavorites(portalId: string) {
return this.dbService
.getByID<{ favorites: Partial<XtreamItem>[] }>(
DbStores.Playlists,
portalId
)
.pipe(map((item) => item.favorites.filter(itm => itm && itm.stream_type && itm.stream_type !== 'live') ?? []));
.getByID<{
favorites: Partial<XtreamItem>[];
}>(DbStores.Playlists, portalId)
.pipe(
map((item) => {
if (!item || !item.favorites) return [];
return item.favorites.filter(
(itm) =>
itm && itm.stream_type && itm.stream_type !== 'live'
);
})
);
}
getPortalLiveStreamFavorites(portalId: string) {
return this.dbService
.getByID<{ favorites: Partial<XtreamItem>[] }>(
DbStores.Playlists,
portalId
)
.pipe(map((item) => item.favorites.filter(itm => itm && itm.stream_type && itm.stream_type === 'live') ?? []));
.getByID<{
favorites: Partial<XtreamItem>[];
}>(DbStores.Playlists, portalId)
.pipe(
map((item) => {
if (!item || !item.favorites) return [];
return item.favorites.filter(
(itm) =>
itm && itm.stream_type && itm.stream_type === 'live'
);
})
);
}
addPortalFavorite(portalId: string, item: any) {
@@ -217,6 +231,7 @@ export class PlaylistsService {
) {
try {
const parsedPlaylist = parse(playlist);
console.log(parsedPlaylist);
return createPlaylistObject(
title,
parsedPlaylist,
+7 -9
View File
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { ApplicationRef, inject, Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SwUpdate } from '@angular/service-worker';
import { Store } from '@ngrx/store';
@@ -19,23 +19,21 @@ import { Playlist } from '../../../shared/playlist.interface';
import { AppConfig } from '../../environments/environment';
import * as PlaylistActions from '../state/actions';
import { DataService } from './data.service';
import { PlaylistsService } from './playlists.service';
@Injectable({
providedIn: 'root',
})
export class PwaService extends DataService {
appRef = inject(ApplicationRef);
playlistService = inject(PlaylistsService);
snackBar = inject(MatSnackBar);
store = inject(Store);
swUpdate = inject(SwUpdate);
translateService = inject(TranslateService);
private readonly http = inject(HttpClient);
private readonly snackBar = inject(MatSnackBar);
private readonly store = inject(Store);
private readonly swUpdate = inject(SwUpdate);
private readonly translateService = inject(TranslateService);
/** Proxy URL to avoid CORS issues */
corsProxyUrl = AppConfig.BACKEND_URL;
constructor(private http: HttpClient) {
constructor() {
super();
console.log('PWA service initialized...');
}
@@ -0,0 +1,51 @@
import { Injectable, computed, signal } from '@angular/core';
import { StorageMap } from '@ngx-pwa/local-storage';
import { Language } from '../settings/language.enum';
import { Settings, VideoPlayer } from '../settings/settings.interface';
import { Theme } from '../settings/theme.enum';
import { STORE_KEY } from '../shared/enums/store-keys.enum';
const DEFAULT_SETTINGS: Settings = {
player: VideoPlayer.VideoJs,
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
mpvPlayerPath: '',
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 3000,
epgUrl: [],
};
@Injectable({
providedIn: 'root',
})
export class SettingsStore {
private settings = signal<Settings>(DEFAULT_SETTINGS);
// Computed values for commonly used settings
readonly player = computed(() => this.settings().player);
readonly showCaptions = computed(() => this.settings().showCaptions);
readonly theme = computed(() => this.settings().theme);
constructor(private storage: StorageMap) {
this.loadSettings();
}
async loadSettings() {
const stored = await this.storage.get(STORE_KEY.Settings).toPromise();
if (stored) {
this.settings.set({ ...DEFAULT_SETTINGS, ...(stored as Settings) });
}
}
async updateSettings(settings: Partial<Settings>) {
const newSettings = { ...this.settings(), ...settings };
this.settings.set(newSettings);
await this.storage.set(STORE_KEY.Settings, newSettings).toPromise();
}
getSettings() {
return this.settings;
}
}
+10 -1
View File
@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { fetch } from '@tauri-apps/plugin-http';
import { parse } from 'iptv-playlist-parser';
import {
EPG_GET_PROGRAM_DONE,
ERROR,
PLAYLIST_PARSE_BY_URL,
PLAYLIST_PARSE_RESPONSE,
@@ -66,6 +67,11 @@ export class TauriService extends DataService {
});
throw error;
});
} else if (type === 'EPG_FETCH_DONE') {
window.postMessage({
type: EPG_GET_PROGRAM_DONE,
payload,
});
} else {
console.log('Unknown type', type);
}
@@ -184,7 +190,10 @@ export class TauriService extends DataService {
}
removeAllListeners(type: string): void {
throw new Error('Method not implemented.');
console.error(
'Method not implemented. Following type was provided:',
type
);
}
listenOn(command: string, callback: (...args: any[]) => void): void {
+22 -16
View File
@@ -1,15 +1,19 @@
<app-header
[title]="'SETTINGS.TITLE' | translate"
[subtitle]="
('SETTINGS.SUBTITLE' | translate) +
'. ' +
('SETTINGS.DESCRIPTION' | translate)
"
></app-header>
@if (!isDialog) {
<app-header
[title]="'SETTINGS.TITLE' | translate"
[subtitle]="
('SETTINGS.SUBTITLE' | translate) +
'. ' +
('SETTINGS.DESCRIPTION' | translate)
"
/>
} @else {
<h2 mat-dialog-title>{{ 'SETTINGS.TITLE' | translate }}</h2>
}
<div class="settings-container" data-test-id="settings-container">
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
<ng-container formArrayName="epgUrl" *ngIf="isElectron">
<ng-container formArrayName="epgUrl" *ngIf="isTauri">
<div class="row">
<div class="column">
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
@@ -205,7 +209,7 @@
></mat-checkbox>
</div>
</div>
@if (isPwa || isElectron) {
@if (isPwa || isTauri) {
<mat-divider />
<div class="row">
<div class="column">
@@ -311,12 +315,14 @@
</div>
</div>
<div class="row" *ngIf="isPwa">
{{ 'SETTINGS.EPG_NOTE' | translate }}
&nbsp;<a href="https://github.com/4gray/iptvnator/releases">{{
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
}}</a>
</div>
@if (isPwa) {
<div class="row">
{{ 'SETTINGS.EPG_NOTE' | translate }}
&nbsp;<a href="https://github.com/4gray/iptvnator/releases">{{
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
}}</a>
</div>
}
<mat-divider />
<div class="action-buttons" align="end">
+56 -65
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Component, Inject, Input, OnInit, Optional } from '@angular/core';
import {
FormArray,
FormBuilder,
@@ -10,6 +11,11 @@ import {
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogModule,
} from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
@@ -22,7 +28,6 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs';
import * as semver from 'semver';
import {
EPG_FORCE_FETCH,
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
SETTINGS_UPDATE,
@@ -32,13 +37,13 @@ import { DataService } from '../services/data.service';
import { DialogService } from '../services/dialog.service';
import { EpgService } from '../services/epg.service';
import { PlaylistsService } from '../services/playlists.service';
import { SettingsStore } from '../services/settings-store.service';
import { HeaderComponent } from '../shared/components/header/header.component';
import { STORE_KEY } from '../shared/enums/store-keys.enum';
import * as PlaylistActions from '../state/actions';
import { selectIsEpgAvailable } from '../state/selectors';
import { SettingsService } from './../services/settings.service';
import { Language } from './language.enum';
import { Settings, VideoPlayer } from './settings.interface';
import { VideoPlayer } from './settings.interface';
import { Theme } from './theme.enum';
@Component({
@@ -58,14 +63,16 @@ import { Theme } from './theme.enum';
MatTooltipModule,
ReactiveFormsModule,
TranslateModule,
MatDialogModule,
],
})
export class SettingsComponent implements OnInit {
@Input() isDialog = false;
/** List with available languages as enum */
languageEnum = Language;
/** Flag that indicates whether the app runs in electron environment */
isElectron = this.electronService.isElectron;
isTauri = this.electronService.getAppEnvironment() === 'tauri';
isPwa = this.electronService.getAppEnvironment() === 'pwa';
@@ -91,7 +98,7 @@ export class SettingsComponent implements OnInit {
label: 'VideoJs Player',
},
...this.electronPlayers,
/* ...(this.isElectron ? this.electronPlayers : []), */
/* ...(this.isTauri ? this.electronPlayers : []), */
];
/** Current version of the app */
@@ -109,7 +116,7 @@ export class SettingsComponent implements OnInit {
/** Settings form object */
settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
...(this.isElectron ? { epgUrl: new FormArray([]) } : {}),
...(this.isTauri ? { epgUrl: new FormArray([]) } : {}),
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
@@ -136,8 +143,13 @@ export class SettingsComponent implements OnInit {
private settingsService: SettingsService,
private snackBar: MatSnackBar,
private store: Store,
private translate: TranslateService
) {}
private translate: TranslateService,
private matDialog: MatDialog,
private settingsStore: SettingsStore,
@Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean }
) {
this.isDialog = data?.isDialog ?? false;
}
/**
* Reads the config object from the browsers
@@ -152,34 +164,12 @@ export class SettingsComponent implements OnInit {
* Sets saved settings from the indexed db store
*/
setSettings(): void {
this.settingsService
.getValueFromLocalStorage(STORE_KEY.Settings)
.subscribe((settings: Settings) => {
if (settings) {
try {
this.settingsForm.setValue({
player: settings.player
? settings.player
: VideoPlayer.VideoJs,
...(this.isElectron ? { epgUrl: [] } : {}),
language: settings.language ?? Language.ENGLISH,
showCaptions: settings.showCaptions ?? false,
theme: settings.theme ?? Theme.LightTheme,
mpvPlayerPath: settings.mpvPlayerPath ?? '',
vlcPlayerPath: settings.vlcPlayerPath ?? '',
remoteControl: settings.remoteControl ?? false,
remoteControlPort:
settings.remoteControlPort ?? 3000,
});
} catch (error) {
throw new Error(error);
}
const currentSettings = this.settingsStore.getSettings()();
this.settingsForm.patchValue(currentSettings);
if (this.isElectron) {
this.setEpgUrls(settings.epgUrl);
}
}
});
if (this.isTauri && currentSettings.epgUrl) {
this.setEpgUrls(currentSettings.epgUrl);
}
}
/**
@@ -248,31 +238,26 @@ export class SettingsComponent implements OnInit {
* the indexed db store
*/
onSubmit(): void {
this.settingsService
.setValueToLocalStorage(
STORE_KEY.Settings,
this.settingsForm.value,
true
)
.pipe(take(1))
.subscribe(() => {
this.applyChangedSettings();
});
this.settingsStore.updateSettings(this.settingsForm.value).then(() => {
this.applyChangedSettings();
this.electronService.sendIpcEvent(
SETTINGS_UPDATE,
this.settingsForm.value
);
this.electronService.sendIpcEvent(
SETTINGS_UPDATE,
this.settingsForm.value
);
this.electronService.sendIpcEvent(
SET_MPV_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
this.electronService.sendIpcEvent(
SET_MPV_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
this.electronService.sendIpcEvent(
SET_VLC_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
this.electronService.sendIpcEvent(
SET_VLC_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
});
if (this.isDialog) {
this.matDialog.closeAll();
}
}
/**
@@ -280,15 +265,17 @@ export class SettingsComponent implements OnInit {
*/
applyChangedSettings(): void {
this.settingsForm.markAsPristine();
// check whether the epg url was changed or not
if (this.isElectron) {
if (this.isTauri) {
let epgUrls = this.settingsForm.value.epgUrl;
if (epgUrls) {
if (!Array.isArray(epgUrls)) {
epgUrls = [epgUrls];
}
epgUrls = epgUrls.filter((url) => url !== '');
this.epgService.fetchEpg(epgUrls);
if (epgUrls.length > 0) {
// Fetch all EPG URLs at once
this.epgService.fetchEpg(epgUrls);
}
}
}
this.translate.use(this.settingsForm.value.language);
@@ -307,7 +294,11 @@ export class SettingsComponent implements OnInit {
* Navigates back to the applications homepage
*/
backToHome(): void {
this.router.navigateByUrl('/');
if (this.isDialog) {
this.matDialog.closeAll();
} else {
this.router.navigateByUrl('/');
}
}
/**
@@ -315,8 +306,7 @@ export class SettingsComponent implements OnInit {
* @param url epg source url
*/
refreshEpg(url: string): void {
this.electronService.sendIpcEvent(EPG_FORCE_FETCH, url);
this.epgService.showFetchSnackbar();
this.epgService.fetchEpg([url]);
}
/**
@@ -394,6 +384,7 @@ export class SettingsComponent implements OnInit {
duration: 2000,
}
);
console.error(error);
}
};
reader.readAsText(file);
@@ -370,33 +370,36 @@ export class StalkerMainContainerComponent implements OnInit {
}
openPlayer(streamUrl: string) {
const player = this.settings()?.player ?? VideoPlayer.VideoJs;
if (player === VideoPlayer.MPV) {
if (!this.hideExternalInfoDialog())
this.dialog.open(ExternalPlayerInfoDialogComponent);
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
url: streamUrl,
mpvPlayerPath: this.settings()?.mpvPlayerPath,
});
} else if (player === VideoPlayer.VLC) {
if (!this.hideExternalInfoDialog())
this.dialog.open(ExternalPlayerInfoDialogComponent);
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
url: streamUrl,
vlcPlayerPath: this.settings()?.vlcPlayerPath,
});
} else {
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
PlayerDialogComponent,
{
data: {
streamUrl,
title: this.itvTitle,
},
width: '80%',
}
);
}
// Get fresh settings directly from storage instead of using signal
this.storage.get(STORE_KEY.Settings).subscribe((settings: Settings) => {
const player = settings?.player ?? VideoPlayer.VideoJs;
if (player === VideoPlayer.MPV) {
if (!this.hideExternalInfoDialog())
this.dialog.open(ExternalPlayerInfoDialogComponent);
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
url: streamUrl,
mpvPlayerPath: settings?.mpvPlayerPath,
});
} else if (player === VideoPlayer.VLC) {
if (!this.hideExternalInfoDialog())
this.dialog.open(ExternalPlayerInfoDialogComponent);
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
url: streamUrl,
vlcPlayerPath: settings?.vlcPlayerPath,
});
} else {
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
PlayerDialogComponent,
{
data: {
streamUrl,
title: this.itvTitle,
},
width: '80%',
}
);
}
});
}
categoryClicked(item: { category_name: string; category_id: string }) {
@@ -36,6 +36,11 @@
'CHANNELS.FAVORITES' | translate
}}</span>
</button>
<button mat-button (click)="openSettings()">
<mat-icon>settings</mat-icon>
<span class="button-label">{{ 'MENU.SETTINGS' | translate }}</span>
</button>
</div>
</div>
<div id="sub-panel">
@@ -59,11 +64,7 @@
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
>
<mat-icon>
{{
isFavoriteStream
? 'star'
: 'star_outline'
}}
{{ isFavoriteStream ? 'star' : 'star_outline' }}
</mat-icon>
</button>
</div>
@@ -91,9 +92,7 @@
</div>
</div>
<mat-menu #sortPlaylistMenu="matMenu">
<section
(click)="$event.stopPropagation()"
>
<section (click)="$event.stopPropagation()">
<button mat-menu-item (click)="setSortType('date')">
<mat-icon>calendar_today</mat-icon>
<span>{{ 'CHANNELS.SORT_BY_DATE' | translate }}</span>
@@ -1,26 +1,43 @@
import { NgFor, NgIf, AsyncPipe } from '@angular/common';
import { Component, EventEmitter, Input, Output, inject, OnChanges, SimpleChanges } from '@angular/core';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
inject,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { Observable, Subject, debounceTime, distinctUntilChanged, map } from 'rxjs';
import {
Observable,
Subject,
debounceTime,
distinctUntilChanged,
map,
} from 'rxjs';
import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interface';
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
import { SettingsComponent } from '../../settings/settings.component';
import { selectCurrentPlaylist } from '../../state/selectors';
import { SettingsStore } from '../../services/settings-store.service';
import { Breadcrumb } from '../breadcrumb.interface';
import { ContentTypeNavigationItem } from '../content-type-navigation-item.interface';
import { ContentType } from '../content-type.enum';
import { PortalStore } from '../portal.store';
import { MatMenuModule } from '@angular/material/menu';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interface';
@Component({
selector: 'app-navigation-bar',
@@ -41,7 +58,7 @@ import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interfac
MatMenuModule,
MatCheckboxModule,
MatTooltipModule,
AsyncPipe
AsyncPipe,
],
})
export class NavigationBarComponent implements OnChanges {
@@ -62,7 +79,7 @@ export class NavigationBarComponent implements OnChanges {
@Output() searchTextChanged = new EventEmitter<string>();
@Output() addToFavoritesClicked = new EventEmitter<any>();
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
ContentTypeEnum = ContentType;
dialog = inject(MatDialog);
portalStore = inject(PortalStore);
@@ -72,6 +89,7 @@ export class NavigationBarComponent implements OnChanges {
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
sortType = this.portalStore.sortType;
isFavoriteStream = false;
settingsStore = inject(SettingsStore);
constructor() {
this.searchPhraseUpdate
@@ -117,7 +135,7 @@ export class NavigationBarComponent implements OnChanges {
name: item.name,
stream_id: item.stream_id,
cover: item.stream_icon,
stream_type: 'live'
stream_type: 'live',
});
this.isFavoriteStream = true;
} else {
@@ -128,17 +146,36 @@ export class NavigationBarComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges) {
if (changes.activeLiveStream && this.activeLiveStream) {
this.checkFavorite();
this.checkFavorite();
}
}
checkFavorite() {
// if activeLiveStream.stream_id include in favorites return the true
const activeLiveStream = this.activeLiveStream;
this.favoritesLiveStream$.pipe(
map(favorites => favorites.some(fav => fav.stream_id === activeLiveStream.stream_id))
).subscribe(isFavorite => {
this.isFavoriteStream = isFavorite;
});
this.favoritesLiveStream$
.pipe(
map((favorites) =>
favorites.some(
(fav) => fav.stream_id === activeLiveStream.stream_id
)
)
)
.subscribe((isFavorite) => {
this.isFavoriteStream = isFavorite;
});
}
openSettings(): void {
this.dialog
.open(SettingsComponent, {
width: '1000px',
height: '90%',
data: { isDialog: true },
})
.afterClosed()
.subscribe(() => {
this.settingsStore.loadSettings();
});
}
}
@@ -1,18 +1,5 @@
import {
AsyncPipe,
KeyValuePipe,
NgFor,
NgIf,
NgSwitch,
} from '@angular/common';
import {
Component,
NgZone,
OnInit,
Signal,
effect,
inject,
} from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Component, NgZone, OnInit, effect, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
@@ -42,10 +29,9 @@ import { EpgItem } from './epg-item.interface';
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';
import { VodDetailsComponent } from './vod-details/vod-details.component';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Router, RouterLink } from '@angular/router';
import { Router } from '@angular/router';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
@@ -56,9 +42,9 @@ import {
import { LiveStreamLayoutComponent } from '../portals/live-stream-layout/live-stream-layout.component';
import { DialogService } from '../services/dialog.service';
import { PlaylistsService } from '../services/playlists.service';
import { Settings, VideoPlayer } from '../settings/settings.interface';
import { SettingsStore } from '../services/settings-store.service';
import { VideoPlayer } from '../settings/settings.interface';
import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component';
import { STORE_KEY } from '../shared/enums/store-keys.enum';
import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component';
import { Breadcrumb, PortalActions } from './breadcrumb.interface';
import { ContentTypeNavigationItem } from './content-type-navigation-item.interface';
@@ -112,12 +98,8 @@ type LayoutView =
styleUrls: ['./xtream-main-container.component.scss'],
standalone: true,
imports: [
KeyValuePipe,
MatButtonToggleModule,
NgFor,
NgIf,
CategoryViewComponent,
NgSwitch,
MatButtonModule,
MatCardModule,
MatIconModule,
@@ -125,11 +107,8 @@ type LayoutView =
VodDetailsComponent,
CategoryContentViewComponent,
SerialDetailsComponent,
PlayerDialogComponent,
MatProgressSpinnerModule,
AsyncPipe,
ExternalPlayerInfoDialogComponent,
RouterLink,
PlaylistErrorViewComponent,
TranslateModule,
LiveStreamLayoutComponent,
@@ -143,6 +122,7 @@ export class XtreamMainContainerComponent implements OnInit {
playlistService = inject(PlaylistsService);
portalStore = inject(PortalStore);
router = inject(Router);
settingsStore = inject(SettingsStore);
snackBar = inject(MatSnackBar);
storage = inject(StorageMap);
store = inject(Store);
@@ -176,9 +156,7 @@ export class XtreamMainContainerComponent implements OnInit {
selectedContentType = ContentType.VODS;
currentLayout: LayoutView = 'category';
vodDetails!: XtreamVodDetails | XtreamSerieDetails;
settings = toSignal(
this.storage.get(STORE_KEY.Settings)
) as Signal<Settings>;
settings = this.settingsStore.getSettings();
isLoading = true;
searchPhrase = this.portalStore.searchPhrase();
contentId: number;
+2 -1
View File
@@ -192,7 +192,8 @@
"DESCRIPTION": "Description",
"LANGUAGE": "Language",
"SHOW_PROGRAM_DETAILS": "Show details about this program"
}
},
"FETCH_SUCCESS": "EPG was fetched successfully"
},
"LANGUAGES": {
"ENGLISH": "English",