├── src ├── index.css ├── components │ ├── Series │ │ ├── Select.css │ │ ├── Episode.js │ │ ├── PlayerSeries.js │ │ └── Seasons.js │ ├── Search │ │ ├── Search.css │ │ └── Search.js │ ├── Player │ │ ├── Player.css │ │ └── Buttons │ │ │ ├── VolumeButton.css │ │ │ ├── VolumeButton.js │ │ │ ├── Button.js │ │ │ └── PiPButton.js │ ├── Epg-Fullscreen │ │ ├── TimeBar │ │ │ ├── TimeBar.js │ │ │ ├── TimeLongLine.js │ │ │ ├── TimeBarIndicator.js │ │ │ └── TimeLine.js │ │ ├── NavBar.js │ │ ├── Day.js │ │ ├── ProgrammeListingVIRTUAL.js.BK │ │ ├── EpgChannel.js │ │ ├── ProgrammeListing.js │ │ ├── Programme-fake.js │ │ ├── ProgrammeVIRTUAL.js.BK │ │ └── Programme.js │ ├── Vod │ │ ├── NextArrow.js │ │ ├── PlayerMag.js │ │ ├── GroupRow.js │ │ ├── PlayerVod.js │ │ └── RowItem.js │ ├── Live │ │ ├── PulsingDot.js │ │ ├── BLUE │ │ │ ├── MainLiveBLUE.js_OLD │ │ │ ├── ChannelEpgBLUE.js_OLD │ │ │ ├── ChannelBLUE.js_OLD │ │ │ └── ChannelsBLUE.js_OLD │ │ ├── EpgListing.js │ │ ├── ChannelEpgBar.js │ │ ├── Tips.js │ │ ├── Epg.js │ │ ├── Channel.js │ │ ├── Channels.js │ │ ├── MainLive.js │ │ └── ChannelEpg.js │ ├── Group │ │ ├── Groups.css │ │ └── Groups.js │ ├── LateralBar │ │ └── LateralBar.css │ ├── MainMenu │ │ ├── MainMenu.js │ │ └── MainMenu.css │ ├── NavBar.js │ ├── Group-old │ │ └── Groups.js │ ├── Popup │ │ └── Popup.js │ ├── AccountInfo.js │ └── Login.js ├── actions │ ├── timer5.js │ ├── timer60.js │ ├── set-Group.js │ ├── h24.js │ ├── epgPopup.js │ ├── set-Playlist.js │ ├── volume.js │ ├── fullScreen.js │ ├── playingChannel.js │ └── set-Playlist-Episodes.js ├── other │ ├── focused-element.js │ ├── group-by.js │ ├── delay.js │ ├── getEpg.js │ ├── convert-day-name.js │ ├── last-opened-mode.js │ ├── convert-rem.js │ ├── Controls │ │ ├── Navbar.js │ │ ├── Group.js │ │ └── Channel.js │ ├── Player-github │ │ └── style.css │ ├── user_info.js │ ├── InfiniteGrid │ │ └── item.js │ ├── generate-url.js │ ├── local-db.js │ ├── key-navigation.js │ ├── vod-series-name-optimizer.js │ ├── axios.js │ ├── Transition.css │ └── auth.js ├── reducer │ ├── groups.js │ ├── playlist.js │ ├── epgPopup.js │ ├── fullScreen.js │ ├── playingChannel.js │ ├── playlist-episodes.js │ ├── timer5s.js │ ├── timer60s.js │ ├── h24.js │ ├── volume.js │ └── index.js ├── index.js ├── App.css ├── App.js └── serviceWorker.js ├── PORT.env ├── .env ├── .env.development ├── .env.production ├── public ├── robots.txt ├── favicon.ico ├── img │ ├── dot.gif │ ├── banner_b.png │ ├── banner_w.png │ ├── no_cover.jpg │ └── placeholder-channel-events_border.svg ├── config.css ├── manifest.json ├── sql_table.sql ├── config.php ├── epg.php ├── README.txt ├── config.js ├── index.html └── proxy.php ├── github-pic ├── top.png ├── vod.png └── channels.png ├── .gitignore ├── package.json └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PORT.env: -------------------------------------------------------------------------------- 1 | PORT=3005 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | -------------------------------------------------------------------------------- /src/components/Series/Select.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_IPTVEDITOR_API=https://api.iptveditor.com/ 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_IPTVEDITOR_API=https://api.iptveditor.com/ 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /github-pic/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/github-pic/top.png -------------------------------------------------------------------------------- /github-pic/vod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/github-pic/vod.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/public/img/dot.gif -------------------------------------------------------------------------------- /github-pic/channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/github-pic/channels.png -------------------------------------------------------------------------------- /public/img/banner_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/public/img/banner_b.png -------------------------------------------------------------------------------- /public/img/banner_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/public/img/banner_w.png -------------------------------------------------------------------------------- /public/img/no_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/HEAD/public/img/no_cover.jpg -------------------------------------------------------------------------------- /src/actions/timer5.js: -------------------------------------------------------------------------------- 1 | export const setTimer5 = () => { 2 | return{ 3 | type: "SET_TIMER_5", 4 | payload: Date.now() 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/timer60.js: -------------------------------------------------------------------------------- 1 | export const setTimer60 = () => { 2 | return{ 3 | type: "SET_TIMER_60", 4 | payload: Date.now() 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/set-Group.js: -------------------------------------------------------------------------------- 1 | export const setGroupList = (groups) => { 2 | return{ 3 | type: "SET_GROUP", 4 | payload: groups 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/h24.js: -------------------------------------------------------------------------------- 1 | export const setH24 = (h24) => { 2 | return{ 3 | type: "SET_H24", 4 | payload: h24 ? "HH:MM" : "hh:MM TT" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/other/focused-element.js: -------------------------------------------------------------------------------- 1 | let focused; 2 | exports.getFocus = () =>{ 3 | return focused; 4 | } 5 | 6 | exports.setFocus = (el) =>{ 7 | focused = el; 8 | } -------------------------------------------------------------------------------- /src/actions/epgPopup.js: -------------------------------------------------------------------------------- 1 | export const setEpgPopup = (payload = null) => { 2 | return{ 3 | type: "SET_EPG_POPUP", 4 | payload: payload 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/set-Playlist.js: -------------------------------------------------------------------------------- 1 | export const setPlaylist = (playlist) => { 2 | return{ 3 | type: "SET_PLAYLIST", 4 | payload: playlist 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/volume.js: -------------------------------------------------------------------------------- 1 | export const setVolume = (add = false) => { 2 | return{ 3 | type: "SET_VOLUME", 4 | payload: add ? 10 : -10 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/fullScreen.js: -------------------------------------------------------------------------------- 1 | export const setFullScreen = (fullscreen) => { 2 | return{ 3 | type: "SET_FULLSCREEN", 4 | payload: fullscreen 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/playingChannel.js: -------------------------------------------------------------------------------- 1 | export const setPlayingChannel = (channel) => { 2 | return{ 3 | type: "SET_CHANNEL", 4 | payload: channel 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/config.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | --second-color: #ec1650; /*use colors easily readable on white background*/ 3 | --second-color-shadow: #ec165008; 4 | --first-color: #171c22; 5 | } 6 | -------------------------------------------------------------------------------- /src/actions/set-Playlist-Episodes.js: -------------------------------------------------------------------------------- 1 | export const setPlaylistEpisodes = (playlist) => { 2 | return{ 3 | type: "SET_PLAYLIST_EPISODES", 4 | payload: playlist 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/other/group-by.js: -------------------------------------------------------------------------------- 1 | export const groupBy = (list, id) => { 2 | return list.reduce(function(results, item) { 3 | (results[item[id]] = results[item[id]] || []).push(item); 4 | return results; 5 | }, {}) 6 | } -------------------------------------------------------------------------------- /src/other/delay.js: -------------------------------------------------------------------------------- 1 | function sleep(delayInms) { 2 | return new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(2); 5 | }, delayInms); 6 | }); 7 | } 8 | 9 | exports.delay = sleep; 10 | exports.sleep = sleep; -------------------------------------------------------------------------------- /src/other/getEpg.js: -------------------------------------------------------------------------------- 1 | exports.getEpg = (epg,id) => { 2 | let toReturn = id && epg && epg.find(x=>id === x.id && x.start <= Date.now() && x.end > Date.now()); 3 | if(!toReturn) 4 | return {start:undefined} 5 | else return toReturn; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/reducer/groups.js: -------------------------------------------------------------------------------- 1 | const groupsReducer = (state = [], action) => { 2 | switch(action.type){ 3 | case "SET_GROUP": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export default groupsReducer; -------------------------------------------------------------------------------- /src/reducer/playlist.js: -------------------------------------------------------------------------------- 1 | const playlistReducer = (state = [], action) => { 2 | switch(action.type){ 3 | case "SET_PLAYLIST": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export default playlistReducer; -------------------------------------------------------------------------------- /src/reducer/epgPopup.js: -------------------------------------------------------------------------------- 1 | const epgPopupReducer = (state = null, action) => { 2 | switch(action.type){ 3 | case "SET_EPG_POPUP": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export default epgPopupReducer; -------------------------------------------------------------------------------- /src/reducer/fullScreen.js: -------------------------------------------------------------------------------- 1 | 2 | const fullScreenReducer = (state = "", action) => { 3 | switch(action.type){ 4 | case "SET_FULLSCREEN": 5 | return action.payload; 6 | default: 7 | return state; 8 | } 9 | } 10 | 11 | export default fullScreenReducer; -------------------------------------------------------------------------------- /src/reducer/playingChannel.js: -------------------------------------------------------------------------------- 1 | 2 | const playingChannelReducer = (state = null, action) => { 3 | switch(action.type){ 4 | case "SET_CHANNEL": 5 | return {...action.payload}; 6 | default: 7 | return state; 8 | } 9 | } 10 | 11 | export default playingChannelReducer; -------------------------------------------------------------------------------- /src/reducer/playlist-episodes.js: -------------------------------------------------------------------------------- 1 | const playlistEpisodeReducer = (state = [], action) => { 2 | switch(action.type){ 3 | case "SET_PLAYLIST_EPISODES": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export default playlistEpisodeReducer; -------------------------------------------------------------------------------- /src/components/Search/Search.css: -------------------------------------------------------------------------------- 1 | .blur-search { 2 | backdrop-filter: blur(.3rem); 3 | background-color: rgba(0, 0, 0, .9); 4 | opacity: 1 !important; 5 | } 6 | 7 | @supports ((-webkit-backdrop-filter: blur(.3em)) or (backdrop-filter: blur(.3em))) { 8 | .blur-search { 9 | background-color: rgba(0, 0, 0, 0) !important; 10 | } 11 | } -------------------------------------------------------------------------------- /src/reducer/timer5s.js: -------------------------------------------------------------------------------- 1 | const timer5Reducer = (state = Date.now(), action) => { 2 | switch(action.type){ 3 | case "SET_TIMER_5": 4 | if(action.payload - state > 1000) 5 | return action.payload; 6 | return state; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export default timer5Reducer; -------------------------------------------------------------------------------- /src/reducer/timer60s.js: -------------------------------------------------------------------------------- 1 | const timer60Reducer = (state = Date.now(), action) => { 2 | switch(action.type){ 3 | case "SET_TIMER_60": 4 | if(action.payload - state > 5000) 5 | return action.payload; 6 | return state; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export default timer60Reducer; -------------------------------------------------------------------------------- /src/other/convert-day-name.js: -------------------------------------------------------------------------------- 1 | const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 2 | 3 | exports.dateToName = (date) =>{ 4 | let d = new Date(date); 5 | return days[d.getDay()]; 6 | } 7 | 8 | exports.dateToReadable = (date) =>{ 9 | let d = new Date(date); 10 | return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}` 11 | } -------------------------------------------------------------------------------- /src/reducer/h24.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const h24Reducer = (state = Cookies.get("h24")||"HH:MM", action) => { 4 | switch(action.type){ 5 | case "SET_H24": 6 | Cookies.set("h24",action.payload,{ expires: 365 }) 7 | return action.payload; 8 | default: 9 | return state; 10 | } 11 | } 12 | 13 | export default h24Reducer; -------------------------------------------------------------------------------- /src/other/last-opened-mode.js: -------------------------------------------------------------------------------- 1 | let oldMode = ""; 2 | let previousGroup = -1; 3 | 4 | exports.resetMemory = (mode) =>{ 5 | if(mode !== oldMode){ 6 | oldMode = mode; 7 | previousGroup = -1; 8 | return true; 9 | }return false; 10 | } 11 | 12 | exports.setGroup = (group) =>{ 13 | previousGroup = group; 14 | } 15 | 16 | exports.getGroup = () => { 17 | return previousGroup; 18 | } -------------------------------------------------------------------------------- /src/other/convert-rem.js: -------------------------------------------------------------------------------- 1 | exports.convertRemToPixels = (rem) => { 2 | return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); 3 | } 4 | 5 | exports.convertVhToPixels = (value) => { 6 | var parts = value.match(/([0-9\.]+)(vh|vw)/) 7 | var q = Number(parts[1]) 8 | var side = window[['innerHeight', 'innerWidth'][['vh', 'vw'].indexOf(parts[2])]] 9 | return side * (q/100) 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /OLD 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/components/Player/Player.css: -------------------------------------------------------------------------------- 1 | .video-overlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background-color: rgba(0, 0, 0, .7); 8 | z-index: 2; 9 | } 10 | 11 | video, 12 | .ie8-poster { 13 | z-index: 1; 14 | background-color: rgba(0, 0, 0, .7); 15 | 16 | } 17 | 18 | .text-copy { 19 | position: absolute; 20 | top: 5%; 21 | left: 5%; 22 | z-index: 2; 23 | text-shadow: 1px 1px 1px #000; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/components/Player/Buttons/VolumeButton.css: -------------------------------------------------------------------------------- 1 | .volume-container:hover > input{ 2 | width: 6em; 3 | visibility: visible; 4 | transition: width .6s, visibility 0s ease; 5 | 6 | } 7 | 8 | .volume-container > input:focus{ 9 | outline: 0!important; 10 | } 11 | 12 | .volume-container > input{ 13 | width:0; 14 | visibility: hidden; 15 | transition: all .6s ease; 16 | margin-left: -0.5em; 17 | } 18 | 19 | .volume-container:hover{ 20 | width: 10em; 21 | } -------------------------------------------------------------------------------- /src/other/Controls/Navbar.js: -------------------------------------------------------------------------------- 1 | import {nextInput} from "../key-navigation" 2 | 3 | export const NavbarKeyboard = { 4 | down: (focus) =>{ 5 | return nextInput(focus,document.querySelectorAll(".lateral-focus"),"lateral-focus", false) || focus; 6 | }, 7 | up: (focus) =>{ 8 | return nextInput(focus,document.querySelectorAll(".lateral-focus"),"lateral-focus", true) || focus; 9 | }, 10 | searchFocus: (focus) =>{ 11 | return document.querySelector(".lateral-focus") 12 | }, 13 | right: () => 1 14 | } 15 | -------------------------------------------------------------------------------- /src/reducer/volume.js: -------------------------------------------------------------------------------- 1 | 2 | const volumeReducer = (state = 50, action) => { 3 | switch(action.type){ 4 | case "SET_VOLUME": 5 | let newVolume = state + action.payload; 6 | if((state >= 100 && newVolume >=100)||(state <= 0 && newVolume <=0)) 7 | return state 8 | else if(newVolume > 100) 9 | newVolume = 100; 10 | else if(newVolume < 0) 11 | newVolume = 0; 12 | return newVolume; 13 | default: 14 | return state; 15 | } 16 | } 17 | 18 | export default volumeReducer; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/other/Controls/Group.js: -------------------------------------------------------------------------------- 1 | export const GroupKeyboard = { 2 | down: (focus) =>{ 3 | if(!focus.nextElementSibling){ 4 | let list = Array.from(document.querySelectorAll(".group")); 5 | return list[list.length-1] 6 | }else return focus.nextElementSibling 7 | }, 8 | up: (focus) =>{ 9 | return focus.previousElementSibling || document.querySelector(".group") || focus; 10 | }, 11 | searchFocus: (focus) =>{ 12 | return document.getElementById("selectedGp") || document.querySelector(".group") 13 | }, 14 | left: () => 1, 15 | enabled: true 16 | } 17 | -------------------------------------------------------------------------------- /public/sql_table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `epg_data`; 2 | 3 | CREATE TABLE `epg_data` ( 4 | `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 5 | `start_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), 6 | `stop_timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', 7 | `title` varchar(255) DEFAULT NULL, 8 | `description` text DEFAULT NULL 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 10 | 11 | 12 | ALTER TABLE `epg_data` 13 | ADD PRIMARY KEY (`id`,`start_timestamp`), 14 | ADD UNIQUE KEY `id` (`id`,`start_timestamp`), 15 | ADD UNIQUE KEY `id_2` (`id`,`stop_timestamp`); 16 | COMMIT; 17 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/TimeBar/TimeBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import TimeBarIndicator from "./TimeBarIndicator" 4 | import TimeLine from "./TimeLine" 5 | 6 | const Container = styled.div` 7 | position: sticky; 8 | display: block; 9 | top: 0px; 10 | margin: 0px; 11 | width: 408rem; 12 | background: #fff; 13 | scroll-behavior: smooth; 14 | box-shadow: 0 3px 8px 0 rgb(0 0 0 / 12%); 15 | ` 16 | 17 | 18 | 19 | const TimeBar = ({day, h24, scrollBtn}) => { 20 | return ( 21 | 22 | 23 | {day===0 && ()} 24 | 25 | ) 26 | } 27 | 28 | export default TimeBar 29 | -------------------------------------------------------------------------------- /public/config.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Player/Buttons/VolumeButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from "./Button" 3 | import "./VolumeButton.css" 4 | 5 | const VolumeButton = ({enabled, volume, onChangeInput, onClick}) => { 6 | return ( 7 |
8 |
19 | ) 20 | } 21 | 22 | export default VolumeButton 23 | -------------------------------------------------------------------------------- /src/other/Player-github/style.css: -------------------------------------------------------------------------------- 1 | .progress-netflix { 2 | -webkit-appearance: none; 3 | width: 100%; 4 | height: .3rem; 5 | outline: none; 6 | opacity: 0.7; 7 | cursor:pointer; 8 | transition: height 0.2s ease, opacity .2s ease; 9 | } 10 | 11 | .progress-netflix:hover { 12 | opacity: 1; 13 | height: .7rem; 14 | } 15 | 16 | .progress-netflix::-webkit-slider-thumb { 17 | -webkit-appearance: none; 18 | appearance: none; 19 | width: 1.4rem; 20 | height: 1.4rem; 21 | border-radius: 50%; 22 | background: var(--second-color)!important; 23 | cursor: pointer; 24 | } 25 | 26 | .progress-netflix::-moz-range-thumb { 27 | width: 1.4rem; 28 | height: 1.4rem; 29 | border-radius: 50%; 30 | background: var(--second-color)!important; 31 | cursor: pointer; 32 | } -------------------------------------------------------------------------------- /src/other/Controls/Channel.js: -------------------------------------------------------------------------------- 1 | let focusMainChannelKeyboard = null; 2 | 3 | 4 | export const MainChannelKeyboard = { 5 | down: (focus) =>{ 6 | if(focus && focus.nextElementSibling) 7 | focusMainChannelKeyboard = focus.nextElementSibling; 8 | return focusMainChannelKeyboard; 9 | }, 10 | up: (focus) =>{ 11 | if(focus && focus.previousElementSibling) 12 | focusMainChannelKeyboard = focus.previousElementSibling; 13 | return focusMainChannelKeyboard; 14 | }, 15 | searchFocus: (focus) =>{ 16 | if(focusMainChannelKeyboard && document.body.contains(focusMainChannelKeyboard)) 17 | return focusMainChannelKeyboard; 18 | return document.getElementById("selectedCh") || document.querySelector(".channel") 19 | }, 20 | backMenu: true, 21 | enabled:false 22 | } 23 | -------------------------------------------------------------------------------- /public/epg.php: -------------------------------------------------------------------------------- 1 | []); 20 | 21 | 22 | 23 | if(!$_POST["epg_id"] || !$_POST["start"] || !$_POST["stop"]){ 24 | echo json_encode($list); 25 | return; 26 | } 27 | 28 | $list->epg_listings = getEpg($_POST["epg_id"], $_POST["start"], $_POST["stop"]); 29 | 30 | 31 | echo json_encode($list); -------------------------------------------------------------------------------- /src/reducer/index.js: -------------------------------------------------------------------------------- 1 | import playingChannelReducer from "./playingChannel"; 2 | import playlistEpisodeReducer from "./playlist-episodes"; 3 | 4 | import groupsReducer from "./groups"; 5 | import playlistReducer from "./playlist"; 6 | import timer60Reducer from "./timer60s"; 7 | import timer5Reducer from "./timer5s"; 8 | import epgPopupReducer from "./epgPopup"; 9 | import h24Reducer from "./h24"; 10 | import {combineReducers} from "redux"; 11 | 12 | const allReducer = combineReducers({ 13 | playingCh : playingChannelReducer, // solo counterReducer => counterReducer : counterReducer 14 | timer60 : timer60Reducer, 15 | timer5 : timer5Reducer, 16 | h24 : h24Reducer, 17 | epgPopup : epgPopupReducer, 18 | groupsList : groupsReducer, 19 | playlist : playlistReducer, 20 | playlistEpisodes : playlistEpisodeReducer, 21 | }) 22 | 23 | export default allReducer; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | import {createStore} from "redux"; 7 | import allReducer from "./reducer/index" 8 | import {Provider} from "react-redux" 9 | 10 | const myStore = createStore( 11 | allReducer, 12 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 13 | ); 14 | 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | 25 | // If you want your app to work offline and load faster, you can change 26 | // unregister() to register() below. Note this comes with some pitfalls. 27 | // Learn more about service workers: https://bit.ly/CRA-PWA 28 | serviceWorker.unregister(); 29 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/TimeBar/TimeLongLine.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import {useSelector} from "react-redux" 4 | import { useState, useEffect, useMemo } from 'react' 5 | 6 | const Line = styled.div` 7 | width: 0.1rem; 8 | position: absolute; 9 | top: 0; 10 | height: 100%; 11 | background: #064497; 12 | ` 13 | 14 | const TimeLongLine = () => { 15 | const timer = useSelector(state => state.timer60); 16 | const date = useMemo(() => new Date(timer),[timer]); 17 | 18 | const [length, setLength] = useState((date.getHours()*17) + (date.getMinutes()*0.25)); 19 | useEffect(() => { 20 | setLength((date.getHours()*17) + (date.getMinutes()*0.25)); 21 | },[timer,date]) 22 | return ( 23 |
24 | 25 |
26 | ) 27 | } 28 | 29 | export default TimeLongLine 30 | -------------------------------------------------------------------------------- /public/README.txt: -------------------------------------------------------------------------------- 1 | 1. Open with a text editor (for example notepad++) config.js and complete empty fields ( window.dns). More instructions are available inside the file. 2 | 2. Open with a text editor (for example notepad++) config.php and write your mysql database info (database url, database name, username, password) and epg xml url. (if you don't use epg you can skip part 2 and 3) 3 | 3. Import "sql_table.sql" in your mysql database (if you are using phpMyAdmin click on "import" -> select the sql_table.sql and click on execute) 4 | 5 | 4. [OPTIONAL] Open with a text editor (for example notepad++) config.css if you want to change main color and background one. 6 | 5. [OPTIONAL] Change favicon.ico and img > banner_w.png 7 | 8 | 6. Copy and paste all the files in your server. Use the root folder, like http://domain.com/ (don't use folders like http://domain.com/player/ !) 9 | 10 | To avoid wasting server resources, epg update will be triggered when an user login and database has less than 12 hours of programmes. 11 | -------------------------------------------------------------------------------- /src/other/user_info.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import * as Axios from "./axios" 3 | 4 | const userInfo = {}; 5 | 6 | export function getInfo(){ 7 | return userInfo; 8 | } 9 | 10 | export function setInfo(data, serverInfo){ 11 | userInfo.expiry = new Date(parseInt(data.exp_date+"000")); 12 | userInfo.username = data.username; 13 | userInfo.password = data.password; 14 | userInfo.max_connections = data.max_connections; 15 | userInfo.message = data.message; 16 | 17 | Axios.post("/epg.php",{init:1, username: userInfo.username, password: userInfo.password, 18 | dns: `${serverInfo.server_protocol}://${serverInfo.url}:${serverInfo.port}` 19 | },true); 20 | 21 | Cookies.set("username",data.username,{ expires: 365 }); 22 | Cookies.set("password",data.password,{ expires: 365 }); 23 | } 24 | 25 | export function logout(){ 26 | Cookies.remove("dns"); 27 | Cookies.remove("username"); 28 | Cookies.remove("password"); 29 | window.location = "/"; 30 | } -------------------------------------------------------------------------------- /src/components/Vod/NextArrow.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from 'styled-components' 3 | import {Link} from "react-router-dom" 4 | 5 | const Li = styled(Link)` 6 | height: calc(22.5vw); 7 | max-height: 60vh; 8 | 9 | width: 15.266667%; 10 | flex: 0 0 15.266667%; 11 | 12 | position: absolute; 13 | right: calc(-7% - 0.4rem); 14 | 15 | padding-left: calc(5% - 0.4rem); 16 | 17 | opacity: 0; 18 | transition: opacity 0.2s ease; 19 | color:white; 20 | 21 | cursor: pointer; 22 | 23 | &:hover, &:focus, & > i:focus{ 24 | opacity: 1; 25 | color:white; 26 | outline-width: 0; 27 | 28 | } 29 | 30 | 31 | 32 | ` 33 | 34 | const I = styled.i` 35 | font-size: 3vw; 36 | padding-top: calc(6.5vw * 1.5); 37 | ` 38 | 39 | 40 | const NextArrow = ({category_id, isSeries}) => { 41 | return ( 42 |
  • 43 | 44 |
  • 45 | ) 46 | } 47 | 48 | export default NextArrow 49 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | /*----- Player name -----*/ 2 | window.playername = "IPTVEditor Web Player"; 3 | 4 | /*----- DNS -----*/ 5 | //Iptv provider dns url (for example "http://domain.com:80") 6 | window.dns = "http://domain.com:80"; 7 | 8 | /*----- CORS -----*/ 9 | /*/ False if iptv provider has the Access-Control-Allow-Origin set on "*" or allows your player domain. 10 | It will work with most of the providers. 11 | 12 | Change in "true" in the other case. Player will use "proxy.php" file to manage the requests. 13 | If true, change also the $cors value in proxy.php 14 | */ 15 | window.cors = false; //true 16 | 17 | /*---- HTTPS -----*/ 18 | /* If streams are using an ssl protol change this box on true.*/ 19 | window.https = false; //true; 20 | 21 | /*----- TMDB API [OPTIONAL] -----*/ 22 | /* By default player will use movie info from the provider. In case these info are missing it will be used tmdb as alternative 23 | Here you can get a tmdb api key: https://developers.themoviedb.org/3/getting-started/introduction */ 24 | window.tmdb = ""; 25 | 26 | 27 | 28 | 29 | 30 | //!!!!!!! Don't change this !!!!!!! 31 | document.getElementsByTagName("title")[0].innerText = window.playername -------------------------------------------------------------------------------- /src/components/Live/PulsingDot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from "styled-components" 3 | 4 | /* 5 | const Pulse = styled.div` 6 | border: .3rem solid #62bd19; 7 | -webkit-border-radius: 99rem; 8 | height: 2rem; 9 | width: 2rem; 10 | position: absolute; 11 | left: -0.55rem; 12 | top: -.5rem; 13 | -webkit-animation: pulsate 1s ease-out; 14 | -webkit-animation-iteration-count: infinite; 15 | opacity: 0.0; 16 | 17 | @keyframes pulsate { 18 | 0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} 19 | 50% {opacity: 1.0;} 20 | 100% {-webkit-transform: scale(1.2, 1.2); opacity: 0.0;} 21 | } 22 | `*/ 23 | 24 | const Circle = styled.div` 25 | content: ""; 26 | top: calc(50%); 27 | display: inline-block; 28 | height: .7rem; 29 | width: .7rem; 30 | border-radius: 50%; 31 | background-color: #b9b9b9; 32 | ` 33 | 34 | 35 | const PulsingDot = ({isPlaying}) => { 36 | 37 | const CircleStyle = {backgroundColor: isPlaying ? "#54ca28" : "b9b9b9"}; 38 | 39 | return ( 40 |
    41 | {isPlaying ? : } 42 |
    43 | ) 44 | } 45 | 46 | export default PulsingDot 47 | -------------------------------------------------------------------------------- /src/components/Live/BLUE/MainLiveBLUE.js_OLD: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Player from "../Player" 3 | import EpgSummary from "../EpgListing" 4 | import Channels from "../Channels" 5 | import styled from "styled-components" 6 | import {useSelector} from "react-redux" 7 | 8 | 9 | const Main = styled.div` 10 | padding: 3vh 1vw; 11 | background-color: /*#171c22;*/ #203d79; 12 | height: calc(100vh - 3rem); 13 | ` 14 | 15 | 16 | const MainLive = () => { 17 | const playingChannel = useSelector(state => state.playingCh); 18 | const fullScreen = useSelector(state => state.fullScreen); 19 | 20 | return ( 21 |
    22 |
    23 |
    24 |
    {playingChannel.name}
    25 | 26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | ) 34 | } 35 | 36 | export default MainLive 37 | -------------------------------------------------------------------------------- /src/other/InfiniteGrid/item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {isEqual} from 'lodash'; 3 | 4 | export default class Item extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | _itemWidth() { 11 | return this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow; 12 | } 13 | 14 | _itemLeft() { 15 | var column = this.props.index % this.props.dimensions.itemsPerRow; 16 | return column * (this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow); 17 | } 18 | 19 | _itemTop() { 20 | return Math.floor(this.props.index / this.props.dimensions.itemsPerRow) * this.props.dimensions.height; 21 | } 22 | 23 | // LIFECYCLE 24 | 25 | shouldComponentUpdate(nextProps, nextState) { 26 | return !isEqual(this.props, nextProps); 27 | } 28 | 29 | // RENDER 30 | 31 | render() { 32 | const _style = { 33 | width: this._itemWidth() - this.props.padding, 34 | height: this.props.dimensions.height - this.props.padding, 35 | left: this._itemLeft(), 36 | top: this._itemTop(), 37 | position: 'absolute' 38 | }; 39 | 40 | var props = { 41 | className: 'item', 42 | style: _style 43 | }; 44 | 45 | return ( 46 |
    47 |
    {this.props.data}
    48 |
    49 | ); 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iptveditor_player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.5.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "axios": "^0.21.1", 11 | "dateformat": "^4.5.1", 12 | "js-cookie": "^2.2.1", 13 | "node-pre-gyp": "^0.17.0", 14 | "qs": "^6.5.2", 15 | "react": "^17.0.1", 16 | "react-dom": "^17.0.1", 17 | "react-icons": "^4.2.0", 18 | "react-player": "^2.9.0", 19 | "react-redux": "^7.2.2", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "^4.0.3", 22 | "react-window": "^1.8.6", 23 | "styled-components": "^5.2.1" 24 | }, 25 | "scripts": { 26 | "start": "set PORT=3006 && react-scripts start", 27 | "build": "set GENERATE_SOURCEMAP=false && react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version", 44 | "iOS 7" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Player/Buttons/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ButtonObj = styled.button` 5 | cursor: pointer; 6 | position: relative; 7 | text-align: center; 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | width: 4em; 12 | -webkit-flex: none; 13 | flex: none; 14 | background: none; 15 | border: none; 16 | color: inherit; 17 | display: inline-block; 18 | font-size: inherit; 19 | line-height: inherit; 20 | text-transform: none; 21 | text-decoration: none; 22 | transition: none; 23 | -webkit-appearance: none; 24 | appearance: none; 25 | transition: text-shadow .2s ease; 26 | 27 | &:focus{ 28 | outline: 0!important; 29 | } 30 | 31 | &:hover{ 32 | text-shadow: 0em 0em 1em white; 33 | } 34 | 35 | ` 36 | 37 | const PopupText = styled.span` 38 | border: 0; 39 | clip: rect(0 0 0 0); 40 | height: 1px; 41 | overflow: hidden; 42 | padding: 0; 43 | position: absolute; 44 | width: 1px; 45 | ` 46 | 47 | const Button = ({onClick, enabled, iconOn, iconOff, textOn, textOff}) => { 48 | return ( 49 | 50 | 51 | {!enabled ? textOn : textOff} 52 | 53 | ) 54 | } 55 | 56 | export default Button 57 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "styled-components" 3 | import Day from "./Day" 4 | import {dateToName, dateToReadable} from "../../other/convert-day-name" 5 | 6 | const Nav = styled.div` 7 | background: #064497; 8 | border: none; 9 | box-shadow: inset 0 5px 10px 0 rgb(0 0 0 / 10%); 10 | ` 11 | 12 | const DaysList = styled.ul` 13 | list-style: none; 14 | padding-left: 0; 15 | margin: 0; 16 | display: flex; 17 | align-items: center; 18 | overflow-x: auto; 19 | display: block; 20 | justify-content: space-between; 21 | flex-flow: row wrap; 22 | align-items: center; 23 | padding: 1rem 2rem; 24 | width: 100%; 25 | 26 | ` 27 | 28 | const NavBar = ({day}) => { 29 | return ( 30 | 39 | ) 40 | } 41 | 42 | 43 | export default NavBar 44 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/Day.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import {Link, useLocation } from "react-router-dom"; 4 | 5 | const DayLi = styled.li` 6 | margin-right: .9375rem; 7 | display: inline; 8 | 9 | & > a{ 10 | padding: 0; 11 | color: #fff; 12 | background-color: transparent; 13 | border-radius: 1.375rem; 14 | text-decoration: none; 15 | white-space: nowrap; 16 | font-size:.9rem 17 | 18 | } 19 | 20 | & > a > span{ 21 | }` 22 | 23 | const selectedDay = { 24 | alignItems: "center", 25 | lineHeight: "1.2", 26 | padding: ".35rem 1.5625rem", 27 | maxHeight: "2.8rem", 28 | borderRadius: "1.375rem", 29 | backgroundColor: "#2359a2", 30 | overflow: "hidden", 31 | textTransform: "uppercase" 32 | } 33 | 34 | 35 | 36 | function Day({name, isSelected, date}) { 37 | const location = useLocation(); 38 | 39 | return ( 40 | 41 | 42 | {name} 43 | 44 | 45 | ) 46 | } 47 | 48 | const generateUrlFromDate = (url, date) =>{ 49 | if(!date) 50 | date = ""; 51 | url = url.replace(/\/$/,""); 52 | let splitted = url.split("/"); 53 | let d = splitted[splitted.length-1]; 54 | if(isNaN(new Date(d).getTime())){ 55 | return url + "/" + date + "/"; 56 | }else return `${splitted.slice(0,-1).join("/")}/${date ? date+"/" : ""}`; 57 | 58 | } 59 | 60 | export default Day 61 | -------------------------------------------------------------------------------- /src/components/Live/EpgListing.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled from "styled-components" 3 | import EpgRow from "./Epg" 4 | import {useSelector} from "react-redux" 5 | import {getEpgNow} from "../../other/epg-database" 6 | 7 | 8 | const EpgSummary = styled.div` 9 | margin-top: 1rem; 10 | max-height: 40vh; 11 | overflow: hidden; 12 | ` 13 | 14 | const EpgListing = ({Epg, Shift}) => { 15 | const timer = useSelector(state => state.timer60); 16 | 17 | const [epgListing, setEpgListing] = useState([]); 18 | 19 | useEffect(()=>{ 20 | const arr = getEpgNow(Epg,Shift); 21 | if(!arr || arr.length === 0 ) 22 | setEpgListing([]); 23 | else setEpgListing(arr); 24 | },[Epg,Shift]); 25 | 26 | 27 | useEffect(() => { 28 | const arr = getEpgNow(Epg,Shift); 29 | if(!arr || arr.length === 0 ) 30 | setEpgListing([]); 31 | else if(arr && arr.length > 0 && epgListing.length>0){ 32 | if(arr[0].start !== epgListing[0].start) 33 | setEpgListing(arr); 34 | }else if(arr && arr.length >0 && (!epgListing || epgListing.length===0)){ 35 | setEpgListing(arr); 36 | } 37 | }, [timer, Epg, Shift]); 38 | 39 | return ( 40 | 41 | {epgListing.slice(0, 4).map((epg,id) => ( 42 | 43 | ))} 44 | 45 | ) 46 | } 47 | 48 | export default EpgListing 49 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/TimeBar/TimeBarIndicator.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import {useSelector} from "react-redux" 3 | import React from "react" 4 | 5 | const dateFormat = require("dateformat"); 6 | 7 | const Container = styled.div` 8 | z-index: 10000000000000; 9 | top: 1.3875rem; 10 | position: absolute; 11 | left: 0; 12 | height: 35px; 13 | 14 | & > div{ 15 | top: 1.1.3875rem; 16 | position: absolute; 17 | left: 0; 18 | height: 35px; 19 | } 20 | ` 21 | const Button = styled.button` 22 | width: 81px; 23 | padding: .25rem 1.5rem; 24 | text-align: center; 25 | color: #fff; 26 | background-color: #064497; 27 | border-radius: 1.875rem; 28 | font-size: .875rem; 29 | white-space: nowrap; 30 | border: none; 31 | width: 5rem; 32 | height: 2rem; 33 | margin: 0; 34 | pointer-events: auto; 35 | ` 36 | 37 | const Line = styled.div` 38 | background: #064497; 39 | height: 2.5rem; 40 | pointer-events: none; 41 | margin: 0 auto; 42 | width: 0.1rem; 43 | 44 | ` 45 | 46 | const TimeBarIndicator = () => { 47 | const timer = useSelector(state => state.timer60); 48 | const h24Format = useSelector(state => state.h24); 49 | const date = new Date(timer); 50 | const length = -1+(date.getHours()*17) + (date.getMinutes()*0.25); 51 | 52 | return ( 53 | 54 |
    55 | 56 | 57 |
    58 |
    59 | ) 60 | } 61 | 62 | export default TimeBarIndicator 63 | -------------------------------------------------------------------------------- /src/components/Vod/PlayerMag.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactNetflixPlayer from "../../other/Player-github/player-github-mag" 3 | import styled from 'styled-components' 4 | import {useParams,useHistory} from "react-router-dom"; 5 | import {optimizeName} from "../../other/vod-series-name-optimizer" 6 | import {useSelector} from "react-redux" 7 | import {initStb, stbVolume, stbFullScreen, stbPlayStream, stbPause, stbGetDuration, stbPosition, stbPositionPercent} from "../../other/stb" 8 | 9 | const Container = styled.div` 10 | position:absolute; 11 | width: 100%; 12 | height: 100%; 13 | top: 0; 14 | left: 0; 15 | ` 16 | 17 | const PlayerMag = () => { 18 | const {category,stream_id} = useParams(); 19 | const history = useHistory(); 20 | stbFullScreen(true); 21 | stbVolume(0) 22 | 23 | const stream = useSelector(state => state.playlist).find(x=>x.stream_id === parseInt(stream_id)); 24 | stbPlayStream(stream.direct_source) 25 | 26 | 27 | return ( 28 | 29 | history.goBack()} 34 | FullPlayer 35 | autoPlay 36 | startPosition={0} 37 | OnEnded={()=> history.goBack()} 38 | overlayEnabled 39 | autoControllCloseEnabled 40 | Style 41 | primaryColor="var(--second-color)" 42 | secundaryColor="var(--first-color)" 43 | /> 44 | 45 | ) 46 | } 47 | 48 | export default PlayerMag 49 | -------------------------------------------------------------------------------- /src/components/Live/ChannelEpgBar.js: -------------------------------------------------------------------------------- 1 | import React,{useEffect, useRef } from 'react' 2 | import styled from "styled-components" 3 | import {useSelector} from "react-redux" 4 | 5 | 6 | const ProgressEpg = styled.div` 7 | display: flex; 8 | height: .2rem; 9 | overflow: hidden; 10 | font-size: .75rem; 11 | background-color: black; 12 | ` 13 | 14 | const ProgressBar = styled.div` 15 | display: -ms-flexbox; 16 | display: flex; 17 | -ms-flex-direction: column; 18 | flex-direction: column; 19 | -ms-flex-pack: center; 20 | justify-content: center; 21 | overflow: hidden; 22 | color: #fff; 23 | text-align: center; 24 | white-space: nowrap; 25 | transition: width .6s ease; 26 | ` 27 | 28 | const ChannelEpgBar = ({start,stop, isPlaying}) => { 29 | const timer = useSelector(state => state.timer5); 30 | 31 | const progress = useRef(0); 32 | const progressEpgStyle = !start ? { 33 | visibility: "hidden", 34 | paddingTop: ".2rem" 35 | } : {}; 36 | 37 | const ProgressBarStyle = isPlaying ? { 38 | backgroundColor: "#fff", 39 | border: "1px solid #829196" 40 | } : { 41 | backgroundColor: "#829196" 42 | }; 43 | ProgressBarStyle.width = (start ? ((timer-start)/(stop-start))*100 + "%" : 0); 44 | 45 | useEffect(() => { 46 | if(start) 47 | progress.current && (progress.current.style.width = ((timer-start)/(stop-start))*100 + "%"); 48 | else progress.current && (progress.current.style.width = 0); 49 | }, [isPlaying,timer,start,stop]); 50 | 51 | return ( 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | export default ChannelEpgBar 59 | -------------------------------------------------------------------------------- /src/components/Vod/GroupRow.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from 'styled-components'; 3 | import RowItem from "./RowItem" 4 | import NextArrow from "./NextArrow" 5 | import {Link} from "react-router-dom" 6 | 7 | const Li = styled.li` 8 | color: white; 9 | height: calc(15vw * 1.7 + 1.75rem); 10 | padding-bottom: 1rem; 11 | 12 | & > div{ 13 | position:relative; 14 | height: 100%; 15 | width: 100%; 16 | } 17 | ` 18 | 19 | const H3 = styled(Link)` 20 | font-size: 1.75rem; 21 | margin-bottom: .5rem; 22 | font-weight: 500; 23 | line-height: 1.2; 24 | margin-top: 0; 25 | color:white; 26 | text-decoration: underline; 27 | text-decoration-color : transparent; 28 | transition: text-decoration-color 0.5s ease; 29 | 30 | 31 | &:focus, &:hover{ 32 | color:white; 33 | } 34 | ` 35 | 36 | 37 | const maxItem = 7; 38 | 39 | const GroupRow = ({category_id, name, style, playlist, isSeries, existingTmdb}) => { 40 | return ( 41 | playlist.length > 0 && ( 42 |
  • 43 |

    {name}

    44 |
    45 | {playlist.slice(0, maxItem).map((x,id)=> 46 | () 47 | )} 48 | {playlist.length > maxItem && ()} 49 |
    50 |
  • 51 | ) 52 | ) 53 | } 54 | 55 | export default GroupRow 56 | -------------------------------------------------------------------------------- /src/components/Live/Tips.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | /* 5 | const Container = styled.div` 6 | position:relative; 7 | bottom:1.5rem; 8 | width:100%; 9 | text-align:center; 10 | color:white; 11 | background-color: var(--first-color); 12 | line-height:1rem; 13 | transform: translate(0%,0%); 14 | ` 15 | 16 | const Box = styled.div` 17 | padding: 0 0.3rem; 18 | margin-right: 0.5rem; 19 | ` 20 | 21 | const Row = styled.div` 22 | place-content: center; 23 | justify-content: center; 24 | ` 25 | 26 | const Column = styled.div` 27 | display: flex; 28 | ` 29 | */ 30 | 31 | const FloatingRow = styled.div` 32 | bottom: .2rem; 33 | position: absolute; 34 | color: white; 35 | width: 50%; 36 | background-color: var(--first-color); 37 | ` 38 | 39 | const Tips = () => { 40 | return ( 41 |
    42 | 43 | 44 | Detailed TV guide 45 | 46 | 47 | 48 | Category 49 | 50 |
    51 | 52 | ) 53 | } 54 | 55 | export default Tips 56 | 57 | /* 58 | 59 | 60 | 61 | 62 | Detailed TV guide 63 | 64 | 65 | 66 | Category 67 | 68 | 69 | 70 | */ -------------------------------------------------------------------------------- /src/components/Live/Epg.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from "styled-components" 3 | import PulsingDot from "./PulsingDot" 4 | import {useSelector} from "react-redux" 5 | 6 | const dateFormat = require("dateformat"); 7 | 8 | const EpgTitle = styled.div` 9 | padding-left: 1rem!important; 10 | white-space: nowrap; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | max-width: 29vw; 14 | ` 15 | const EpgDescription = styled.div` 16 | font-weight: 100; 17 | overflow-wrap: break-word; 18 | overflow: hidden; 19 | /*word-wrap: break-word; 20 | word-break: break-all;*/ 21 | text-overflow: ellipsis; 22 | display: -webkit-box; 23 | -webkit-box-orient: vertical; 24 | -webkit-line-clamp: 6; 25 | padding-right: 12px; 26 | ` 27 | const EpgList = styled.div` 28 | & > div { 29 | place-self: center; 30 | padding-right: 0; 31 | padding-left: 0; 32 | } 33 | ` 34 | 35 | 36 | function EpgRow({title,description,start,stop,first}) { 37 | const h24Format = useSelector(state => state.h24); 38 | 39 | return ( 40 |
    41 | 42 |
    43 | Date.now()}> 44 |
    45 |
    46 | {dateFormat(new Date(start), h24Format)} - {dateFormat(new Date(stop), h24Format)} 47 |
    48 |
    49 | {title} 50 |
    51 |
    52 | {first && ( 53 |
    54 | {description} 55 |
    56 | )} 57 |
    58 | ) 59 | } 60 | 61 | export default EpgRow 62 | -------------------------------------------------------------------------------- /src/components/Player/Buttons/PiPButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ButtonObj = styled.button` 5 | cursor: pointer; 6 | position: relative; 7 | text-align: center; 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | width: 4em; 12 | -webkit-flex: none; 13 | flex: none; 14 | background: none; 15 | border: none; 16 | color: inherit; 17 | display: inline-block; 18 | font-size: inherit; 19 | line-height: inherit; 20 | text-transform: none; 21 | text-decoration: none; 22 | transition: none; 23 | -webkit-appearance: none; 24 | appearance: none; 25 | transition: text-shadow .2s ease; 26 | margin-left: auto; 27 | &:focus{ 28 | outline: 0!important; 29 | } 30 | 31 | &:hover{ 32 | text-shadow: 0em 0em 1em white; 33 | } 34 | 35 | ` 36 | 37 | const PopupText = styled.span` 38 | border: 0; 39 | clip: rect(0 0 0 0); 40 | height: 1px; 41 | overflow: hidden; 42 | padding: 0; 43 | position: absolute; 44 | width: 1px; 45 | ` 46 | 47 | const PiPButton = ({onClick, enabled}) => { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {!enabled ? "Picture in Picture" : "Exit Picture in Picture"} 57 | 58 | ) 59 | } 60 | 61 | export default PiPButton 62 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/ProgrammeListingVIRTUAL.js.BK: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import {useSelector, useDispatch} from "react-redux" 4 | import Programme from "./Programme" 5 | import { VariableSizeList as List } from "react-window"; 6 | import {convertRemToPixels} from "../../other/convert-rem" 7 | 8 | const Container = styled.ul` 9 | width: calc(5950px); 10 | display: block; 11 | position: relative; 12 | background-image: url(/img/placeholder-channel-events_border.svg); 13 | background-repeat: repeat-x; 14 | background-size: contain; 15 | height: 6.625rem; 16 | list-style-type:none; 17 | min-width: 357rem; 18 | max-width: 357rem; 19 | 20 | &:before{ 21 | content: ""; 22 | background-image: url(/img/placeholder-channel-events_border.svg); 23 | background-repeat: repeat-x; 24 | width: 500px; 25 | height: 100%; 26 | position: absolute; 27 | left: -504px; 28 | } 29 | ` 30 | 31 | const ProgrammeListing = ({epgId, today, tomorrow, moveFocus}) => { 32 | const epg = useSelector(state => state.epgList); 33 | const epgListing = epg.filter(epg=>epg.id === epgId && epg.start > today && epg.start { 36 | let val = convertRemToPixels(((epgListing[index].end-epgListing[index].start)/3600000)*15) 37 | return val; 38 | } 39 | 40 | const Row = ({ index, style }) => ( 41 | 42 | ); 43 | 44 | return ( 45 | 46 | 53 | {Row} 54 | 55 | 56 | ) 57 | } 58 | 59 | export default ProgrammeListing 60 | -------------------------------------------------------------------------------- /src/other/generate-url.js: -------------------------------------------------------------------------------- 1 | import {getInfo} from "./user_info" 2 | import {getDns, getIsIptveditor} from "./axios" 3 | 4 | export function generateUrl (type, id, extension){ 5 | const dns = getIsIptveditor()===true ? process.env.REACT_APP_IPTVEDITOR_API : getDns(); 6 | const user = getInfo().username; 7 | const pass = getInfo().password; 8 | return `${dns}${type}/${user}/${pass}/${id}.${ getIsIptveditor()===true ? "mp4" : extension}` 9 | } 10 | 11 | export function convertTsToM3u8(ip) { 12 | let url = ip; 13 | url = url.replace("/live/","/").replace(/\.ts|\.m3u8/g,"") 14 | const splitted = url.split("/"); 15 | 16 | const id = splitted[splitted.length-1]; 17 | if(isNaN(id)) 18 | return ip; 19 | const user = splitted[splitted.length-3]; 20 | const pass = splitted[splitted.length-2]; 21 | const domain = splitted.slice(0,splitted.length-3).join("/"); 22 | 23 | return domain + "/live/" + user + "/" + pass + "/" + id + ".m3u8"; 24 | } 25 | 26 | export function catchupUrlGenerator (ip,time, duration){ 27 | let url = ip; 28 | time = timeConverter(time) 29 | url = url.replace("/live/","/").replace(/\.ts|\.m3u8/g,"") 30 | const splitted = url.split("/"); 31 | 32 | const id = splitted[splitted.length-1]; 33 | if(isNaN(id)) 34 | return ip; 35 | const user = splitted[splitted.length-3]; 36 | const pass = splitted[splitted.length-2]; 37 | const domain = splitted.slice(0,splitted.length-3).join("/"); 38 | 39 | return domain + "/timeshift/" + user + "/" + pass + "/" + duration + "/" + time.split(":").slice(0,-1).join(":").replace(" ",":") + "/" + id + ".m3u8"; 40 | } 41 | 42 | const timeConverter = (time) =>{ 43 | const a = new Date(time); 44 | const year = a.getFullYear(); 45 | const month = a.getMonth()+1; 46 | const day = a.getDate(); 47 | const hour = a.getHours(); 48 | const min = a.getMinutes(); 49 | const sec = a.getSeconds(); 50 | 51 | return year + "-" + (month < 10 ? "0"+month : month) + "-" + (day < 10 ? "0"+day : day) + " " + (hour < 10 ? "0"+hour : hour) + ':' + (min < 10 ? "0"+min : min) + ':' + (sec < 10 ? "0"+sec : sec); 52 | }; 53 | 54 | export default {generateUrl, catchupUrlGenerator} 55 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | IPTVEditor Web Player 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
    42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/other/local-db.js: -------------------------------------------------------------------------------- 1 | import {getInfo} from "./user_info" 2 | 3 | let history = {live:[], movie:[], series:[]}; 4 | let favorite = {live:[], movie:[], series:[]}; 5 | 6 | export const initDb = () =>{ 7 | let txt = localStorage.getItem("history-" + getInfo().username); 8 | txt ? (history = JSON.parse(txt)) : saveDB(0); 9 | 10 | txt = localStorage.getItem("favorite-" + getInfo().username); 11 | txt ? (favorite = JSON.parse(txt)) : saveDB(1); 12 | } 13 | 14 | const findOne = (type, key, fav) =>{ 15 | const db = getDb(fav); 16 | const el = db[type].find(x=>parseInt(x.id) === parseInt(key)) 17 | if(!el) 18 | return null; 19 | return el && fav !== true ? {...el, date: new Date(el.date)} : el; 20 | } 21 | 22 | const findAll = (type, fav) =>{ 23 | if(fav !== true) 24 | return history[type].sort((a,b)=> a.date-b.date).map(x=> {return {...x, date : new Date(x.date)}}); 25 | else return favorite[type]; 26 | } 27 | 28 | const set = (type,key,value, fav) =>{ 29 | const db = getDb(fav); 30 | 31 | fav !== true && (value = {...value, date: Date.now()}) 32 | 33 | const i = db[type].findIndex(x=>x.id === key); 34 | i !== -1 ? db[type][i] = value : db[type].unshift(value) 35 | 36 | reorderDB(fav); 37 | saveDB(fav); 38 | } 39 | 40 | const del = (type, key, fav) =>{ 41 | if(fav !== true) 42 | history[type] = history[type].filter(x=> parseInt(x.id) !== parseInt(key)); 43 | else favorite[type] = favorite[type].filter(x=> parseInt(x.id) !== parseInt(key)); 44 | 45 | saveDB(fav); 46 | } 47 | 48 | const reorderDB = (fav) =>{ 49 | if (fav !== true) { 50 | history.live = history.live.slice(0, 20).sort((a, b) => b.date - a.date) 51 | history.movie = history.movie.slice(0, 20).sort((a, b) => b.date - a.date) 52 | history.series = history.series.slice(0, 20).sort((a, b) => b.date - a.date) 53 | }else{ 54 | favorite.live = favorite.live.slice(0, 30) 55 | favorite.movie = favorite.movie.slice(0, 30) 56 | favorite.series = favorite.series.slice(0, 30) 57 | } 58 | } 59 | 60 | const saveDB = (fav) =>{ 61 | window.localStorage.setItem((fav !== true ? "history-" : "favorite-") + getInfo().username, JSON.stringify(fav !== true ? history : favorite)); 62 | } 63 | 64 | const getDb = fav => fav !== true ? history : favorite 65 | 66 | export default {findOne, findAll, set, del} -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1000px) { 2 | /*MAG*/ 3 | html { 4 | font-size: 62.5%; 5 | } 6 | } 7 | 8 | html { 9 | font-family: "Proxima Nova", sans-serif; 10 | overflow: hidden; 11 | } 12 | 13 | * { 14 | scrollbar-color: #aaa transparent; 15 | scrollbar-width: thin; 16 | } 17 | 18 | *::-webkit-scrollbar { 19 | width: 12px; 20 | border-radius: 6px; 21 | } 22 | 23 | *::-webkit-scrollbar-track { 24 | border-radius: 6px; 25 | background-color: transparent; 26 | } 27 | 28 | *::-webkit-scrollbar-thumb { 29 | background-clip: padding-box; 30 | background-color: #aaa; 31 | border: 2px solid transparent; 32 | min-height: 50px; 33 | border-radius: 6px; 34 | } 35 | 36 | 37 | body { 38 | width: 100vw; 39 | height: 100vh; 40 | background-color: #fff0; 41 | max-width: 100vw; 42 | } 43 | 44 | .alert-enter { 45 | opacity: 0; 46 | transform: scale(0.9); 47 | } 48 | 49 | .alert-enter-active { 50 | opacity: 0; 51 | transform: translateX(0); 52 | transition: opacity 300ms, transform 300ms; 53 | } 54 | 55 | .alert-exit { 56 | opacity: 0; 57 | } 58 | 59 | .alert-exit-active { 60 | opacity: 0; 61 | transform: scale(0.9); 62 | transition: opacity 300ms, transform 300ms; 63 | } 64 | 65 | 66 | .isPlayingChannel { 67 | color: #fff; 68 | } 69 | 70 | .isPlayingChannel:hover { 71 | color: #000; 72 | cursor: pointer; 73 | } 74 | 75 | .isPlayingChannel:hover * { 76 | color: #000; 77 | cursor: pointer; 78 | } 79 | 80 | #selectedCh * { 81 | color: white; 82 | } 83 | 84 | .hg-keyMarker{ 85 | box-shadow: 0 0 0 2px var(--second-color) !important; 86 | } 87 | 88 | 89 | 90 | .alert-enter { 91 | opacity: 0; 92 | transform: scale(0.9); 93 | } 94 | .alert-enter-active { 95 | opacity: 1; 96 | transform: translateX(0); 97 | transition: opacity 300ms, transform 300ms; 98 | } 99 | .alert-exit { 100 | opacity: 1; 101 | } 102 | .alert-exit-active { 103 | opacity: 0; 104 | transform: scale(0.9); 105 | transition: opacity 300ms, transform 300ms; 106 | } 107 | 108 | 109 | .center-container{ 110 | display: inline-flex; 111 | align-content: center; 112 | justify-content: center; 113 | align-items: center; 114 | } 115 | 116 | body > iframe{ 117 | display: none; 118 | } -------------------------------------------------------------------------------- /src/other/key-navigation.js: -------------------------------------------------------------------------------- 1 | let comands = {}; 2 | let toFocusName = null; 3 | 4 | export function setComands(comand){ 5 | document.removeEventListener("keydown",navigation) 6 | document.addEventListener("keydown", navigation) 7 | comands = comand; 8 | toFocusName = {parent:comand.parent, child:comand.child}; 9 | comands.focus(); 10 | } 11 | 12 | 13 | export function navigation(event, comands){ 14 | event.preventDefault(); 15 | event.stopImmediatePropagation(); 16 | if (event.keyCode === 39) { //right 17 | comands.right && (comands.right()); 18 | } else if(event.keyCode === 37){ //left 19 | comands.left && (comands.left()); 20 | } else if(event.keyCode === 40){ //down 21 | comands.down && (comands.down()); 22 | } else if(event.keyCode === 38){ //up 23 | comands.up && (comands.up()); 24 | } else if(event.keyCode === 107 || event.keyCode === 109){ //volume 25 | comands.volume && (comands.volume(event.keyCode === 107 || event.keyCode === 109)); 26 | } else if(event.keyCode === 27 || event.keyCode === 8){ //back 27 | comands.back && (comands.back()); 28 | } 29 | } 30 | 31 | export function findParentBySelector(elm, selector) { 32 | var all = document.querySelectorAll(selector); 33 | var cur = elm.parentNode; 34 | while(cur && !collectionHas(all, cur)) { //keep going up until you find a match 35 | cur = cur.parentNode; //go up 36 | } 37 | return cur; //will return null if not found 38 | } 39 | 40 | 41 | function collectionHas(a, b) { //helper function (see below) 42 | for(var i = 0, len = a.length; i < len; i ++) { 43 | if(a[i] === b) return true; 44 | } 45 | return false; 46 | } 47 | 48 | export function nextInput(currentInput, inputArray, inputClass, reverse) { 49 | inputArray = Array.from(inputArray) 50 | let i,j, array = reverse ? inputArray.reverse() : inputArray; 51 | for (i = 0; i < inputArray.length - 1; i++) { 52 | if(currentInput === array[i]) { 53 | for (j = 1; j < inputArray.length - i; j++) { 54 | //Check if the next element exists and if it has the desired class 55 | if(array[i + j] && (array[i + j].className === inputClass)) { 56 | return array[i + j]; 57 | break; 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/EpgChannel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import ProgrammeListing from "./ProgrammeListing" 4 | 5 | const Container = styled.div` 6 | display: block; 7 | margin-top: 1rem; 8 | ` 9 | 10 | const Channel = styled.div` 11 | display: inline-block; 12 | z-index: 2; 13 | padding-bottom: .5rem; 14 | position: -webkit-sticky; 15 | position: sticky; 16 | left: 160px; 17 | z-index: 1; 18 | line-height:.4rem; 19 | transition: all 0.1s ease 0s; 20 | 21 | & > button{ 22 | box-sizing: border-box; 23 | border: 1px solid #e5e5e5; 24 | border-radius: .75rem; 25 | background-color: #fff; 26 | display: flex; 27 | flex-flow: row nowrap; 28 | align-items: center; 29 | margin-bottom: 0; 30 | text-decoration: none; 31 | color: #303030; 32 | } 33 | 34 | & > button > p{ 35 | padding-right: 1rem; 36 | margin-top: revert; 37 | margin-bottom: revert; 38 | } 39 | ` 40 | 41 | const ChannelLogo = styled.div` 42 | padding-right: .5rem; 43 | margin-right: .5rem; 44 | padding-left: .5rem; 45 | color: #fff; 46 | border-right: .1rem solid #e5e5e5; 47 | 48 | & > img{ 49 | display: block; 50 | height: 100%; 51 | max-height: 1.625rem; 52 | width: auto; 53 | max-width: 100%; 54 | } 55 | ` 56 | 57 | 58 | 59 | const EpgChannel = ({Name,Image, Epg, Shift, day, moveFocus, style, leftScroll, chId, catchup}) => { 60 | let today = new Date() 61 | today.setDate(today.getDate()+day); 62 | today.setHours(0) 63 | today.setMinutes(0) 64 | today.setSeconds(0) 65 | today.setMilliseconds(0) 66 | let tomorrow = new Date(today); 67 | tomorrow.setDate(tomorrow.getDate()+1) 68 | 69 | return ( 70 | 71 | 72 | 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | export default EpgChannel 85 | -------------------------------------------------------------------------------- /src/other/vod-series-name-optimizer.js: -------------------------------------------------------------------------------- 1 | export function optimizeName(name) { 2 | name = name.replace("\r", "").replace(/(-\s*\d{2,4})|vod|fhd|hd|360p|4k|h264|h265|24fps|60fps|720p|1080p|vod|x264|x265|\.avi|\.mp4|\.mkv|\[.*]|\(.*\)|\{.*\}|-|_|\./gim, " ").replace(/(- \d\d\d\d$)/, ""); 3 | name = name.replace(" ", " "); 4 | name = name.replace(" ", " "); 5 | return name.trim(); 6 | } 7 | 8 | export function secondsToHms(d) { 9 | d = Number(d); 10 | var h = Math.floor(d / 3600); 11 | var m = Math.floor(d % 3600 / 60); 12 | var s = Math.floor(d % 3600 % 60); 13 | return `${h}:${m < 10 ? `0${m}` : m}:${s < 10 ? `0${s}` : s}`; 14 | } 15 | 16 | export function createMovieInfo(name, image, tmdbId = -1, releaseDate = "", coverBig = "", youtubeTrailer = "", director = "", actors = "", cast = "", description = "", plot = "", age = "", country = "", genre = "", backdropPath = "", durationSecs = 0, duration = "", rating10 = 0, status = "Released", runtime = 0) { 17 | return { 18 | "info": { 19 | "name": name, 20 | "tmdb_id": tmdbId, 21 | "cover_big": coverBig, 22 | "movie_image": image, 23 | "releasedate": releaseDate, 24 | "youtube_trailer": youtubeTrailer, 25 | "director": director, 26 | "actors": actors, 27 | "cast": cast, 28 | "description": description, 29 | "plot": plot, 30 | "age": age, 31 | "country": country, 32 | "genre": genre, 33 | "backdrop_path": [ 34 | backdropPath 35 | ], 36 | "duration_secs": durationSecs, 37 | "duration": duration, 38 | "rating": rating10, 39 | "status": status, 40 | "runtime": runtime 41 | }, 42 | } 43 | } 44 | 45 | export function createEpisode(url, id, season, episode_number, duration_sec = 0, duration = "", title = "", airdate = "", crew = "", rating = "0", image = "", overview) { 46 | return { //season 47 | "episode_num": episode_number, 48 | "title": title === "" ? `Episode ${episode_number}` : title, 49 | "container_extension": "mp4", 50 | "url": url, 51 | "id": id, 52 | "overview": overview, 53 | "info": { 54 | "air_date": airdate, 55 | "crew": crew, 56 | "rating": parseInt(rating), 57 | "id": id, 58 | "movie_image": image, 59 | "duration_secs": duration_sec, 60 | "duration": duration, 61 | }, 62 | "season": season, 63 | } 64 | } -------------------------------------------------------------------------------- /src/components/Vod/PlayerVod.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import ReactNetflixPlayer from "../../other/Player-github/player-github" 3 | import styled from 'styled-components' 4 | import {useParams,useHistory} from "react-router-dom"; 5 | import {optimizeName} from "../../other/vod-series-name-optimizer" 6 | import {useSelector} from "react-redux" 7 | import DB from "../../other/local-db" 8 | import {generateUrl} from "../../other/generate-url" 9 | 10 | const Container = styled.div` 11 | position:absolute; 12 | width: 100%; 13 | height: 100%; 14 | top: 0; 15 | left: 0; 16 | ` 17 | 18 | const PlayerVod = () => { 19 | const {category,stream_id} = useParams(); 20 | const history = useHistory(); 21 | 22 | const stream = useSelector(state => state.playlist).find(x=>x.stream_id === parseInt(stream_id)); 23 | const [streamStat,setStreamStat] = useState(DB.findOne("movie",stream_id)); 24 | 25 | const [url, setUrl] = useState(); 26 | 27 | useEffect(()=>{ 28 | if(!stream){ 29 | history.replace(`/movie/`); 30 | return; 31 | } 32 | if(stream.direct_source) 33 | setUrl(stream.direct_source) 34 | else{ 35 | setUrl(generateUrl("movie",stream.stream_id,stream.container_extension)) 36 | } 37 | },[stream]) 38 | 39 | useEffect(()=>{ 40 | let stat = DB.findOne("movie",stream_id); 41 | !stat && (stat={id: stream_id, start:0, tot:0}) 42 | setStreamStat(stat) 43 | DB.set("movie",stream_id, stat) 44 | },[category, stream_id]) 45 | 46 | const setStat = (stat) =>{ 47 | setStreamStat(stat); 48 | DB.set("movie",stream_id, stat) 49 | } 50 | 51 | 52 | return ( 53 | 54 | history.goBack()} 59 | FullPlayer 60 | autoPlay 61 | startPosition={streamStat ? parseInt(streamStat.start) : 0} 62 | syncDuration = {(duration, percentage) => setStat({...streamStat, start:duration, tot : parseInt(percentage)})} 63 | 64 | OnEnded={()=> {DB.del("movie",stream_id);history.goBack()}} 65 | overlayEnabled 66 | autoControllCloseEnabled 67 | Style 68 | primaryColor="var(--second-color)" 69 | secundaryColor="var(--first-color)" 70 | /> 71 | 72 | ) 73 | } 74 | 75 | export default PlayerVod 76 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/ProgrammeListing.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import styled from 'styled-components' 3 | import Programme from "./Programme" 4 | import ProgrammeFake from "./Programme-fake" 5 | import {getEpg, downloadEpgData} from "../../other/epg-database" 6 | 7 | const Container = styled.ul` 8 | width: calc(408rem); 9 | display: block; 10 | position: relative; 11 | background-image: url(/img/placeholder-channel-events_border.svg); 12 | background-repeat: repeat-x; 13 | background-size: contain; 14 | height: 6.625rem; 15 | list-style-type:none; 16 | min-width: 408rem; 17 | max-width: 408rem; 18 | 19 | &:before{ 20 | content: ""; 21 | background-image: url(/img/placeholder-channel-events_border.svg); 22 | background-repeat: repeat-x; 23 | width: 500px; 24 | height: 100%; 25 | position: absolute; 26 | left: -504px; 27 | } 28 | ` 29 | 30 | const ProgrammeListing = ({Epg, Shift, today, moveFocus,chId, day, liveTime, catchup}) => { 31 | const [epgListing, setEpgListing] = useState([]); 32 | const catchupDate = catchup > 0 ? Date.now() - (catchup * 1000 * 60 * 60 * 24) : 0 33 | 34 | useEffect(() => { 35 | async function fetchData(d) { 36 | if (!Epg) 37 | return; 38 | await downloadEpgData(chId, Epg, d+1, Shift); 39 | 40 | const newEpg = getEpg(Epg, d+1, Shift); 41 | if (newEpg) { 42 | setEpgListing([...newEpg]); 43 | } else setEpgListing([]); 44 | } 45 | fetchData(day); 46 | }, [day]) 47 | 48 | 49 | 50 | return ( 51 | 52 | {epgListing.length === 0 ? 53 | fakeEpgGenerator(today).map((epg) => ( 54 | () 55 | )) 56 | : 57 | epgListing.map((epg,id) => ( 58 | 0 && epg.start >= catchupDate && epg.start <= Date.now()}/> 59 | )) 60 | } 61 | 62 | ) 63 | } 64 | 65 | export default ProgrammeListing 66 | 67 | 68 | const fakeEpgGenerator = (today) =>{ 69 | let toReturn = []; 70 | for(let i = 0; i < 24; i+=3){ 71 | toReturn.push({start: today+(i*3600000), end: today+((i+3)*3600000)}) 72 | } 73 | return toReturn; 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamity.tv v2 Xtream IPTV Player (by IPTVEditor.com) 2 | 3 | ![](https://github.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/raw/master/github-pic/top.png) 4 | 5 | [![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://github.com/lKinderBueno/StreamityTV-Xtream) 6 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CVT6HXLZ3YNSG&source=url) 7 | 8 | 9 | ### Official Streamity.tv website: [Click here](https://streamity.tv) 10 | 11 | Streamity is an online webplayer. Watch IPTV Channels, movies TV series online on your browser with no additional software required! 12 | 13 | ### DISCLAIMER: Streamity is a player. No stream or movies is included. Pictures are just for demo purpouse. 14 | 15 | ### Features 16 | - Xtream Api support 17 | - Customizable name and logo 18 | - Modern design and smooth animations 19 | - EPG Viewer 20 | - 12h/24h EPG time format support 21 | - Pic in picture player 22 | - Automatic fix and improve movie and series name 23 | - Favorites 24 | - Continue to watch for vod and series 25 | - Automatic select next series episode 26 | - React js 27 | 28 | ### Other Pictures 29 | ![](https://github.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/raw/master/github-pic/channels.png?1) 30 | ![](https://github.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/raw/master/github-pic/vod.png) 31 | 32 | 33 | ### Installation 34 | 1. Download latest release (streamity-v2.X.zip): [Click here](https://github.com/lKinderBueno/Streamity-Xtream-IPTV-Web-player/releases) 35 | 2. Open with a text editor (for example notepad++) config.js and complete empty fields ( window.dns). More instructions are available inside the file. 36 | 3. Open with a text editor (for example notepad++) config.php and write your mysql database info (database url, database name, username, password) and epg xml url. (if you don't use epg you can skip part 2 and 3) 37 | 4. Import "sql_table.sql" in your mysql database (if you are using phpMyAdmin click on "import" -> select the sql_table.sql and click on execute) 38 | 39 | 5. [OPTIONAL] Open with a text editor (for example notepad++) config.css if you want to change main color and background one. 40 | 6. [OPTIONAL] Change favicon.ico and img > banner_w.png 41 | 42 | 7. Copy and paste all the files in your server. Use the root folder, like http://domain.com/ (don't use folders like http://domain.com/player/ !) 43 | 44 | ### Installation in sub-folder (ex: http://domain.com/player/ ) 45 | Copy the "static" folder in your root folder and all the other files in /player/ 46 | 47 | 48 | 49 | 50 | To avoid wasting server resources, epg update will be triggered when an user login and database has less than 12 hours of programmes. 51 | -------------------------------------------------------------------------------- /src/other/axios.js: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import Cookies from 'js-cookie' 3 | import qs from "qs" 4 | 5 | let dns = null; 6 | let proxyRequired = window.cors; 7 | let isIptveditor = null; 8 | 9 | setDns(window.dns); 10 | 11 | export async function get(url, timeout = 1) { 12 | return Axios.get(url).catch(err => err); 13 | } 14 | 15 | export async function post(url, params = {}, local, useProxy) { 16 | if (!dns) 17 | return null; 18 | 19 | let uri = url 20 | 21 | if (local === true && !window.location.origin.match(/iptveditor\.com|localhost/)) { 22 | return Axios.post(uri, qs.stringify(params)).catch(err => err) 23 | } 24 | 25 | if (isIptveditor === false) { 26 | uri += "?"; 27 | for (const key in params) { 28 | uri += key + "=" + encodeURIComponent(params[key]) + "&"; 29 | } 30 | } 31 | 32 | if ((proxyRequired || useProxy) === true && isIptveditor === true) 33 | uri = "/proxy.php?url=" + encodeURIComponent(dns); 34 | else if ((proxyRequired || useProxy) === true && isIptveditor === false) 35 | uri = "/proxy.php?url=" + encodeURIComponent(dns + uri); 36 | else if (isIptveditor === false) 37 | uri = dns + uri; 38 | else uri = dns; 39 | 40 | return isIptveditor === true && !(proxyRequired || useProxy)? 41 | Axios.post(uri, qs.stringify(params)) : 42 | Axios.get(uri, { 43 | headers: { 44 | 'User-Agent': 'Mozilla/6.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.55 Safari/537.36 Edge/86.0.622.28' 45 | }, 46 | timeout: 25000 47 | }).catch(err => { 48 | if(proxyRequired === false && !useProxy && !err.response) 49 | return post(url, params, local, true) 50 | return err 51 | }); 52 | 53 | } 54 | 55 | 56 | export function setDns(data) { 57 | if (!data) 58 | return; 59 | if (window.location.protocol !== 'https:' && data.includes("https")) 60 | data = data.replace("https", "http"); 61 | else if (window.location.protocol === 'https:' && !data.includes("https")) 62 | data = data.replace("http", "https"); 63 | 64 | isIptveditor = !!data.match(/iptveditor\.com|xtream-ie|opop\.pro|localhost|192\.168\.178\.71\:3100/) 65 | 66 | if (window.isDebug === 1) 67 | data = window.dns; 68 | else if (isIptveditor === true && window.isDebug !== 1) 69 | data = `${process.env.REACT_APP_IPTVEDITOR_API}webplayer`; 70 | else { 71 | if (data[data.length - 1] !== "/") 72 | data += "/"; 73 | } 74 | 75 | dns = data; 76 | Cookies.set("dns", data, { expires: 365 }) 77 | } 78 | 79 | export function getDns() { 80 | return dns; 81 | } 82 | 83 | export function getIsIptveditor() { 84 | return isIptveditor; 85 | } -------------------------------------------------------------------------------- /src/components/Live/BLUE/ChannelEpgBLUE.js_OLD: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import {useSelector} from "react-redux" 3 | import { useState, useEffect } from 'react' 4 | import {getEpg} from "../../other/getEpg" 5 | import ChannelEpgBar from "./ChannelEpgBar" 6 | const dateFormat = require("dateformat"); 7 | const HeaderChannel = styled.div` 8 | padding-bottom: .4rem!important; 9 | line-height: 1!important; 10 | font-size: 0.65rem; 11 | color: #598ede; 12 | font-weight: 700; 13 | ` 14 | 15 | const BodyChannel = styled.div` 16 | text-transform: capitalize; 17 | color: #fff; 18 | overflow: hidden; 19 | white-space: nowrap; 20 | text-overflow: ellipsis; 21 | ` 22 | const Title = styled.label` 23 | font-size: 1rem; 24 | max-width: 100%; 25 | white-space: nowrap; 26 | overflow: hidden; 27 | text-overflow: ellipsis;` 28 | 29 | const TitleEpg = styled.label` 30 | padding-left: 2%; 31 | font-size: 0.9rem; 32 | font-weight: 100; 33 | max-width: 70%; 34 | white-space: nowrap; 35 | overflow: hidden; 36 | text-overflow: ellipsis;` 37 | 38 | const PlayingButton = styled.i` 39 | font-size: 2.2rem; 40 | color: #e9d454; 41 | ` 42 | 43 | const ChannelEpg = ({chId,Name,Epg,isPlaying}) => { 44 | const epg = useSelector(state => state.epgList); 45 | const h24Format = useSelector(state => state.h24); 46 | const timer = useSelector(state => state.timer60); 47 | const [epgNow, setEpgNow] = useState(getEpg(epg,Epg)); 48 | 49 | let tomorrow = new Date() 50 | tomorrow.setDate(tomorrow.getDate()+1) 51 | tomorrow.setHours(0) 52 | tomorrow.setMinutes(0) 53 | 54 | useEffect(() => { 55 | const newEpg = getEpg(epg,Epg); 56 | if(epgNow.start && epgNow.start !== newEpg.start){ 57 | setEpgNow({...newEpg}); 58 | } 59 | }, [timer,epgNow.start,epg,Epg]); 60 | 61 | return ( 62 | <> 63 |
    64 | 65 | {epgNow.start && `${dateFormat(new Date(epgNow.start), h24Format)} - ${dateFormat(new Date(epgNow.end), h24Format)}`} 66 | 67 | 68 | <b>{Name}</b> 69 | {epgNow.title} 70 | 71 |
    72 |
    73 | {isPlaying && ()} 74 |
    75 |
    76 | {epgNow.start ? 77 | () 78 | : 79 | 80 | } 81 |
    82 | 83 | ) 84 | } 85 | 86 | export default ChannelEpg 87 | 88 | -------------------------------------------------------------------------------- /src/components/Group/Groups.css: -------------------------------------------------------------------------------- 1 | .netflix-text { 2 | text-transform: uppercase; 3 | } 4 | 5 | .netflix-nav-rev-btn { 6 | border: 0; 7 | background: transparent; 8 | cursor: pointer; 9 | font-size: 1.5rem; 10 | } 11 | 12 | 13 | .netflix-nav-rev { 14 | position: fixed; 15 | top: 0; 16 | right: -5%; 17 | height: 100vh; 18 | transform: translateX(100%); 19 | transition: transform .3s ease-in-out; 20 | z-index: 1000000000; 21 | } 22 | 23 | .netflix-nav-rev.visible { 24 | transform: translateX(0); 25 | } 26 | 27 | .netflix-nav-rev-black { 28 | background-color: var(--second-color); 29 | width: 60%; 30 | max-width: 35rem; 31 | min-width: 20rem; 32 | transition-delay: .4s; 33 | } 34 | 35 | .netflix-nav-rev-black.visible { 36 | transition-delay: 0s; 37 | } 38 | 39 | .netflix-nav-rev-red { 40 | background-color: var(--first-color); 41 | transition-delay: .2s; 42 | width: 95%; 43 | } 44 | 45 | .netflix-nav-rev-red.visible { 46 | transition-delay: .2s; 47 | } 48 | 49 | .netflix-nav-rev-white { 50 | background-color: #fff; 51 | padding: 3rem 0rem 0rem 2rem; 52 | position: relative; 53 | transition-delay: 0s; 54 | width: 95%; 55 | } 56 | 57 | .netflix-nav-rev-white.visible { 58 | transition-delay: .4s; 59 | } 60 | 61 | .netflix-close-btn-reverse { 62 | position: unset; 63 | } 64 | 65 | .netflix-close-btn:focus, .netflix-close-btn:hover, .netflix-close-btn:hover > i { 66 | color: var(--second-color); 67 | outline-width: 0px; 68 | } 69 | 70 | .netflix-logo { 71 | width: 10rem; 72 | } 73 | 74 | .netflix-list { 75 | list-style-type: none; 76 | padding: 0; 77 | height: 100%; 78 | overflow: auto; 79 | } 80 | 81 | .netflix-list-reverse { 82 | margin-right: 8rem; 83 | 84 | } 85 | 86 | .netflix-list-reverse li { 87 | margin: 1rem 0; 88 | padding-left: 0.5rem; 89 | } 90 | 91 | .netflix-list li button { 92 | color: var(--first-color); 93 | font-size: 1rem; 94 | text-decoration: none; 95 | text-transform: uppercase; 96 | transition: color 0.2s ease; 97 | } 98 | 99 | .netflix-list li button:hover, .netflix-list li button:focus{ 100 | color: var(--second-color); 101 | } 102 | 103 | 104 | .netflix-list ul { 105 | list-style-type: none; 106 | padding-left: 1.4rem; 107 | } 108 | 109 | 110 | 111 | .floating-btn { 112 | border-radius: 26.1rem; 113 | background-color: #001F61; 114 | border: .1rem solid #001F61; 115 | box-shadow: 0 1.3rem 1.5rem -1.6rem #03153B; 116 | color: #fff; 117 | cursor: pointer; 118 | font-size: 1.3rem; 119 | line-height: 1.4rem; 120 | padding: 1.2rem 1.4rem; 121 | position: fixed; 122 | bottom: 1.4rem; 123 | right: 1.4rem; 124 | z-index: 999; 125 | } 126 | 127 | .floating-btn:hover { 128 | background-color: #ffffff; 129 | color: #001F61; 130 | } 131 | 132 | .floating-btn:focus { 133 | outline: none; 134 | } 135 | 136 | @media screen and (max-width: 44rem) { 137 | 138 | .social-panel-container.visible { 139 | transform: translateX(0); 140 | } 141 | 142 | .floating-btn { 143 | right: 1rem; 144 | } 145 | } 146 | 147 | * { 148 | box-sizing: border-box; 149 | } -------------------------------------------------------------------------------- /src/components/LateralBar/LateralBar.css: -------------------------------------------------------------------------------- 1 | .netflix-text { 2 | text-transform: uppercase; 3 | } 4 | 5 | .lateral-focus, .lateral-focus:focus{ 6 | background-color: transparent; 7 | border: none; 8 | outline: none; 9 | 10 | } 11 | 12 | .netflix-nav-btn { 13 | border: 0; 14 | background: transparent; 15 | cursor: pointer; 16 | font-size: 1.5rem; 17 | } 18 | 19 | 20 | .netflix-nav { 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | height: 100vh; 25 | transform: translateX(-100%); 26 | transition: transform .3s ease-in-out; 27 | z-index: 1000000000; 28 | } 29 | 30 | .netflix-nav.visible { 31 | transform: translateX(0); 32 | } 33 | 34 | .netflix-nav-black { 35 | background-color: var(--first-color); 36 | width: 60%; 37 | max-width: 35rem; 38 | min-width: 20rem; 39 | transition-delay: .4s; 40 | } 41 | 42 | .netflix-nav-black.visible { 43 | transition-delay: 0s; 44 | } 45 | 46 | .netflix-nav-red { 47 | background-color: var(--second-color); 48 | transition-delay: .2s; 49 | width: 95%; 50 | } 51 | 52 | .netflix-nav-red.visible { 53 | transition-delay: .2s; 54 | } 55 | 56 | .netflix-nav-white { 57 | background-color: #fff; 58 | padding: 3rem; 59 | position: relative; 60 | transition-delay: 0s; 61 | width: 95%; 62 | } 63 | 64 | .netflix-nav-white.visible { 65 | transition-delay: .4s; 66 | } 67 | 68 | .netflix-close-btn { 69 | outline-width: 0px; 70 | opacity: 0.3; 71 | position: absolute; 72 | top: 3rem; 73 | right: 2.5rem; 74 | transition: color 0.5s ease; 75 | } 76 | 77 | .netflix-close-btn:focus, .netflix-close-btn:hover, .netflix-close-btn:hover > i { 78 | color: var(--second-color); 79 | } 80 | 81 | .netflix-logo { 82 | width: 10rem; 83 | } 84 | 85 | .netflix-list { 86 | list-style-type: none; 87 | padding: 0; 88 | } 89 | 90 | .netflix-list li { 91 | margin: 1.5rem 0; 92 | } 93 | 94 | .netflix-list li button { 95 | color: var(--first-color); 96 | font-size: 1rem; 97 | text-decoration: none; 98 | text-transform: uppercase; 99 | transition: color 0.2s ease; 100 | } 101 | 102 | .netflix-list li button:hover, .netflix-list li button:focus{ 103 | color: var(--second-color); 104 | } 105 | 106 | 107 | .netflix-list ul { 108 | list-style-type: none; 109 | padding-left: 1.4rem; 110 | } 111 | 112 | 113 | 114 | .floating-btn { 115 | border-radius: 26.1rem; 116 | background-color: #001F61; 117 | border: .1rem solid #001F61; 118 | box-shadow: 0 1.3rem 1.5rem -1.6rem #03153B; 119 | color: #fff; 120 | cursor: pointer; 121 | font-size: 1.3rem; 122 | line-height: 1.4rem; 123 | padding: 1.2rem 1.4rem; 124 | position: fixed; 125 | bottom: 1.4rem; 126 | right: 1.4rem; 127 | z-index: 999; 128 | } 129 | 130 | .floating-btn:hover { 131 | background-color: #ffffff; 132 | color: #001F61; 133 | } 134 | 135 | .floating-btn:focus { 136 | outline: none; 137 | } 138 | 139 | @media screen and (max-width: 44rem) { 140 | 141 | .social-panel-container.visible { 142 | transform: translateX(0); 143 | } 144 | 145 | .floating-btn { 146 | right: 1rem; 147 | } 148 | } 149 | 150 | * { 151 | box-sizing: border-box; 152 | } -------------------------------------------------------------------------------- /src/other/Transition.css: -------------------------------------------------------------------------------- 1 | .hvr-reveal { 2 | display: inline-block; 3 | vertical-align: middle; 4 | -webkit-transform: perspective(1px) translateZ(0); 5 | transform: perspective(1px) translateZ(0); 6 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 7 | position: relative; 8 | overflow: hidden; 9 | } 10 | 11 | .hvr-reveal:before, 12 | .hvr-reveal:after { 13 | content: ""; 14 | position: absolute; 15 | z-index: -1; 16 | left: 0; 17 | right: 0; 18 | top: 0; 19 | bottom: 0; 20 | border-color: var(--second-color); 21 | border-style: solid; 22 | border-width: 0; 23 | -webkit-transition-property: border-width; 24 | transition-property: border-width; 25 | -webkit-transition-duration: 0.1s; 26 | transition-duration: 0.1s; 27 | -webkit-transition-timing-function: ease-out; 28 | transition-timing-function: ease-out; 29 | } 30 | 31 | .hvr-reveal:hover:before, 32 | .hvr-reveal:focus:before, 33 | .hvr-reveal:active:before { 34 | -webkit-transform: translateY(0); 35 | transform: translateY(0); 36 | border-width: 4px; 37 | } 38 | 39 | .hvr-grow-shadow { 40 | display: inline-block; 41 | vertical-align: middle; 42 | -webkit-transform: perspective(1px) translateZ(0); 43 | transform: perspective(1px) translateZ(0); 44 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 45 | -webkit-transition-duration: 0.3s; 46 | transition-duration: 0.3s; 47 | -webkit-transition-property: box-shadow, transform; 48 | transition-property: box-shadow, transform; 49 | } 50 | .hvr-grow-shadow:hover, .hvr-grow-shadow:focus, .hvr-grow-shadow:active { 51 | box-shadow: 0 10px 10px -10px rgba(0, 0, 0, 0.5); 52 | -webkit-transform: scale(1.1); 53 | transform: scale(1.1); 54 | } 55 | 56 | 57 | 58 | /* Shutter In Horizontal */ 59 | .hvr-shutter-in-horizontal { 60 | display: inline-block; 61 | vertical-align: middle; 62 | -webkit-transform: perspective(1px) translateZ(0); 63 | transform: perspective(1px) translateZ(0); 64 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 65 | position: relative; 66 | background: white; 67 | -webkit-transition-property: color; 68 | transition-property: color; 69 | -webkit-transition-duration: 0.3s; 70 | transition-duration: 0.3s; 71 | transition: all 0.3s ease-out; 72 | 73 | } 74 | 75 | 76 | .hvr-shutter-in-horizontal:before { 77 | content: ""; 78 | position: absolute; 79 | z-index: -1; 80 | top: 0; 81 | bottom: 0; 82 | left: 0; 83 | right: 0; 84 | background: var(--first-color); 85 | -webkit-transform: scaleX(1); 86 | transform: scaleX(1); 87 | -webkit-transform-origin: 50%; 88 | transform-origin: 50%; 89 | -webkit-transition-property: transform; 90 | transition-property: transform; 91 | -webkit-transition-duration: 0.3s; 92 | transition-duration: 0.3s; 93 | -webkit-transition-timing-function: ease-out; 94 | transition-timing-function: ease-out; 95 | } 96 | 97 | 98 | .hvr-shutter-in-horizontal-on *{ 99 | color:black !important; 100 | } 101 | 102 | .hvr-shutter-in-horizontal-on:before { 103 | -webkit-transform: scaleX(0); 104 | transform: scaleX(0); 105 | } 106 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react" 2 | import {HashRouter as Router, Route,Switch} from "react-router-dom" 3 | import {PrivateRoute,ProvideAuth} from "./other/auth" 4 | import "./App.css" 5 | 6 | import NavBar from "./components/NavBar" 7 | import MainMenu from "./components/MainMenu/MainMenu" 8 | import AccountInfo from "./components/AccountInfo" 9 | import Login from "./components/Login" 10 | 11 | import LateralBar from "./components/LateralBar/LateralBar" 12 | 13 | import MainLive from "./components/Live/MainLive" 14 | import Groups from "./components/Group/Groups" 15 | import Search from "./components/Search/Search" 16 | import EpgFullListing from "./components/Epg-Fullscreen/EpgFullListing" 17 | 18 | import MainVod from "./components/Vod/MainVod" 19 | 20 | import {useDispatch} from "react-redux" 21 | import {setTimer60} from "./actions/timer60" 22 | import {setTimer5} from "./actions/timer5" 23 | 24 | 25 | function App() { 26 | const dispatch = useDispatch() 27 | setInterval(() => dispatch(setTimer60()), 50000); 28 | setInterval(() => dispatch(setTimer5()), 5000); 29 | 30 | if(window.location.protocol !== 'https:' && window.https===true) 31 | window.location = window.location.href.replace("http","https"); 32 | else if(window.location.protocol === 'https:' && window.https===false) 33 | window.location = window.location.href.replace("https","http"); 34 | 35 | let url = window.location.hash.replace("#",""); 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export default App; 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/components/Live/BLUE/ChannelBLUE.js_OLD: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import ChannelEpg from "../ChannelEpg" 3 | import {useRef} from 'react' 4 | 5 | const Li = styled.li` 6 | /*border-radius: .4rem;*/ 7 | vertical-align: middle; 8 | display: inline-flex; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | &:focus{ 13 | outline-width: 0px; 14 | background-color: #203d79; 15 | transition: all .3s ease-in-out; 16 | cursor: pointer; 17 | } 18 | 19 | &:hover{ 20 | background-color: #203d79; 21 | transition: all .3s ease-in-out; 22 | cursor: pointer; 23 | } 24 | 25 | & + & { 26 | margin-top: 10px; 27 | border-top: #203d79 1px solid; 28 | padding-top: 10px; 29 | padding-bottom: 10px; 30 | } 31 | 32 | ` 33 | 34 | const ChannelNumber = styled.div` 35 | align-self: center; 36 | text-align: left; 37 | padding: 0; 38 | padding-left: 1rem; 39 | 40 | & > div{ 41 | font-size: 1rem !important; 42 | } 43 | ` 44 | const HeaderChannel = styled.div` 45 | padding-bottom: .4rem!important; 46 | line-height: 1!important; 47 | font-size: 1rem; 48 | font-weight: 700; 49 | ` 50 | const Logo = styled.div` 51 | background: #203d79; 52 | padding: .1rem .1rem; 53 | border-radius: .2rem; 54 | color: #fff; 55 | text-align: center; 56 | width: 100%; 57 | font-weight: bold; 58 | min-height: 2.4rem; 59 | max-height: 2.4rem; 60 | 61 | &>img{ 62 | max-height: 2.2rem; 63 | height: auto; 64 | width: auto; 65 | padding-left: 0.5rem; 66 | padding-right: 0.5rem; 67 | } 68 | ` 69 | 70 | const ChannelContainer = styled.div` 71 | /*padding-bottom: .2rem; 72 | padding-top: .2rem;*/ 73 | ` 74 | 75 | const Channel = ({ Name, Image, Epg, Number, id, selected, selectEvent, fullScreenEvent, keydown, virtualScrollStyle }) => { 76 | const inputEl = useRef(null); 77 | 78 | const style = {} 79 | if(selected){ 80 | style.backgroundColor = "#203d79"; 81 | style.transition = "all .3s ease-in-out"; 82 | style.cursor = "pointer"; 83 | style.color = "#e9d454!important" 84 | } 85 | 86 | 87 | 88 | return ( 89 |
  • 90 | 91 |
    92 | 93 | 94 | {Number} 95 | 96 | 97 |
    98 | 99 | {this.onerror=null;this.src='../images/nologo.png?1'}} */ align="middle" alt="" /> 100 | 101 |
    102 | 103 |
    104 |
    105 |
  • 106 | 107 | ) 108 | } 109 | 110 | export default Channel 111 | -------------------------------------------------------------------------------- /src/components/MainMenu/MainMenu.js: -------------------------------------------------------------------------------- 1 | import React, {useState,useEffect} from 'react' 2 | import styled from "styled-components" 3 | import {useHistory} from 'react-router-dom' 4 | 5 | import "./MainMenu.css" 6 | const Container = styled.div` 7 | background-color: #212529; 8 | position:absolute; 9 | width:100%; 10 | height:100%; 11 | top:0; 12 | left:0; 13 | ` 14 | 15 | const Box = styled.div` 16 | position: fixed; 17 | z-index: 98; 18 | &:before, &:after { 19 | content: ""; 20 | position: fixed; 21 | width: 100vw; 22 | height: 100vh; 23 | background: rgba(20, 21, 26,0.6); 24 | border-bottom-left-radius: 200%; 25 | z-index: -1; 26 | -webkit-transition: -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 27 | transition: -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 28 | transition: transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 29 | transition: transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 30 | -webkit-transform: translateX(100%) translateY(-100%); 31 | transform: translateX(100%) translateY(-100%); 32 | } 33 | &:after { 34 | background: rgba(9,9,12,1); 35 | -webkit-transition-delay: 0s; 36 | transition-delay: 0s; 37 | } 38 | &:before { 39 | -webkit-transition-delay: .2s; 40 | transition-delay: .2s; 41 | } 42 | ` 43 | 44 | const MainMenu = () => { 45 | const [style, setStyle] = useState(); 46 | const history = useHistory(); 47 | 48 | useEffect(() => { 49 | setTimeout(()=>{ 50 | setStyle("nav-active"); 51 | document.querySelector(".nav__list-item") && (document.querySelector(".nav__list-item").focus()); 52 | },10) 53 | },[]); 54 | 55 | const redirect = (page) =>{ 56 | setStyle(); 57 | setTimeout(()=>{ 58 | history.push(page) 59 | },600) 60 | } 61 | 62 | const remoteController = (event) =>{ 63 | const active = document.activeElement; 64 | if (event.keyCode === 40 && active.nextSibling) { 65 | active.nextSibling.focus(); 66 | } else if (event.keyCode === 38 && active.previousSibling) { 67 | active.previousSibling.focus(); 68 | } 69 | //clearTimeout(timeout); 70 | } 71 | 72 | /*const stopRemote = () =>{ 73 | clearTimeout(timeout); 74 | }*/ 75 | 76 | const moveFocus = (event) => { 77 | remoteController(event); 78 | } 79 | 80 | return ( 81 | 82 | 83 |
    84 |
      85 |
    • redirect("/live/category/")}>
    • 86 |
    • redirect("/movie/")}>
    • 87 |
    • redirect("/series/")}>
    • 88 |
    • redirect("/info/")}>
    • 89 |
    90 |
    91 |
    92 |
    93 | ) 94 | } 95 | 96 | export default MainMenu 97 | -------------------------------------------------------------------------------- /src/components/Live/BLUE/ChannelsBLUE.js_OLD: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import {useSelector, useDispatch} from "react-redux" 3 | import Channel from "../Channel" 4 | import {setPlaylingChannel} from "../../../actions/playingChannel" 5 | import {setFullScreen} from "../../../actions/fullScreen" 6 | import {setVolume} from "../../../actions/volume" 7 | import { FixedSizeList as List } from "react-window"; 8 | import {convertRemToPixels,convertVhToPixels} from "../../../other/convert-rem" 9 | import {useEffect} from 'react' 10 | 11 | const ChannelsList = styled.ul` 12 | height: calc(95vh - 3rem); 13 | padding: .8rem .6rem; 14 | background-color: #162f65; 15 | border-radius: 5px; 16 | list-style-type: none; 17 | 18 | &:focus{ 19 | outline-width: 0px; 20 | } 21 | ` 22 | 23 | const Channels = () => { 24 | const channelsPlaylist = useSelector(state => state.playlist); 25 | const isFullScreen = useSelector(state => state.fullScreen); 26 | const playingChannel = useSelector(state => state.playingCh); 27 | 28 | const dispatch = useDispatch() 29 | 30 | let timeout; 31 | const stopRemote = () =>{ 32 | clearTimeout(timeout); 33 | } 34 | 35 | const moveFocus = (event) => { 36 | remoteController(event); 37 | timeout = setTimeout(moveFocus, 500, event); 38 | } 39 | 40 | const remoteController = (event) => { 41 | const active = document.activeElement; 42 | if (event.keyCode === 40 && active.nextSibling) { 43 | active.nextSibling.focus(); 44 | } else if (event.keyCode === 38 && active.previousSibling) { 45 | active.previousSibling.focus(); 46 | } else if (event.keyCode === 107 || event.keyCode === 109) { 47 | dispatch(setVolume(event.keyCode === 107)) 48 | } else if(event.keyCode === 27 || event.keyCode === 8){ //BACK 49 | 50 | } else if(event.keyCode === 39){ //RIGHT 51 | 52 | } 53 | clearTimeout(timeout); 54 | } 55 | 56 | useEffect(() => { 57 | document.getElementById("selectedCh") && (document.getElementById("selectedCh").focus()) 58 | }, [isFullScreen]); 59 | 60 | let interval; 61 | const Row = ({ index, style }) => ( 62 | { 63 | dispatch(setPlaylingChannel(channelsPlaylist[index])) 64 | interval = setInterval(() => {document.getElementById("selectedCh").focus();clearInterval(interval)}, 200) 65 | }} fullScreenEvent={() => dispatch(setFullScreen(true))} onKeyDown={moveFocus} onKeyUp={stopRemote} virtualScrollStyle={style} selected={playingChannel.stream_id === channelsPlaylist[index].stream_id} /> 66 | ); 67 | 68 | return ( 69 | 70 | 77 | {Row} 78 | 79 | 80 | 81 | 82 | ) 83 | } 84 | 85 | 86 | 87 | export default Channels 88 | -------------------------------------------------------------------------------- /src/components/Live/Channel.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react" 2 | import styled from "styled-components" 3 | import ChannelEpg from "./ChannelEpg" 4 | import "../../other/Transition.css" 5 | 6 | const Li = styled.li` 7 | /*border-radius: .4rem;*/ 8 | vertical-align: middle; 9 | display: inline-flex; 10 | justify-content: center; 11 | align-items: center; 12 | color: #829196; 13 | width: 90% !important; 14 | left: 5% !important; 15 | border-bottom: #000 1px solid; 16 | 17 | &:focus, &:hover{ 18 | outline-width: 0px; 19 | /*background-color: #fff; 20 | transition: background-color .3s ease-in-out; 21 | color: #020d18 !important; 22 | border-radius: 5px;*/ 23 | cursor: pointer; 24 | border-bottom: #000 0px solid !important; 25 | } 26 | 27 | &:hover *, &:focus *{ 28 | /*color: #020d18 !important;*/ 29 | cursor :pointer; 30 | } 31 | 32 | 33 | & + & { 34 | margin-top: 10px; 35 | padding-top: 10px; 36 | padding-bottom: 10px; 37 | } 38 | 39 | ` 40 | 41 | const ChannelNumber = styled.div` 42 | align-self: center; 43 | text-align: left; 44 | padding: 0; 45 | padding-left: 1rem; 46 | 47 | & > div{ 48 | font-size: 1rem !important; 49 | } 50 | ` 51 | const HeaderChannel = styled.div` 52 | padding-bottom: .4rem!important; 53 | line-height: 1!important; 54 | font-size: 1rem; 55 | font-weight: 700; 56 | ` 57 | const Logo = styled.div` 58 | /*background: #203d79;*/ 59 | padding: .1rem .1rem; 60 | border-radius: .2rem; 61 | text-align: center; 62 | width: 100%; 63 | font-weight: bold; 64 | min-height: 3rem; 65 | max-height: 3rem; 66 | 67 | background-position-x: center; 68 | background-position-y: center; 69 | background-size: contain; 70 | background-repeat: no-repeat; 71 | 72 | &>img{ 73 | max-height: 3rem; 74 | height: auto; 75 | width: auto; 76 | padding-left: 0.5rem; 77 | padding-right: 0.5rem; 78 | } 79 | ` 80 | 81 | const ChannelContainer = styled.div` 82 | /*padding-bottom: .2rem; 83 | padding-top: .2rem;*/ 84 | ` 85 | 86 | const Channel = ({ Name, Image, Epg, Shift, Number, id, selected, selectEvent, virtualScrollStyle }) => { 87 | const style = {} 88 | 89 | useEffect(()=>{ 90 | if(selected){ 91 | setTimeout(() => { 92 | document.getElementById("selectedCh") && (document.getElementById("selectedCh").className += " hvr-shutter-in-horizontal-on") 93 | }, 100) 94 | } 95 | },[]) 96 | 97 | 98 | 99 | return ( 100 |
  • 101 | 102 |
    103 | 104 | 105 | {Number} 106 | 107 | 108 |
    109 | 110 | {//{this.onerror=null;this.src='../images/nologo.png?1'}} */ align="middle" alt="" /> 111 | } 112 | 113 |
    114 | 115 |
    116 |
    117 |
  • 118 | 119 | ) 120 | } 121 | 122 | export default Channel 123 | -------------------------------------------------------------------------------- /public/img/placeholder-channel-events_border.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | placeholder-channel-events_border 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/Programme-fake.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import {useSelector} from "react-redux" 4 | 5 | const dateFormat = require("dateformat"); 6 | 7 | const Container = styled.li` 8 | position: absolute; 9 | left: 0; 10 | height: 100%; 11 | background-color: #fff; 12 | transition: all 0.3s ease 0s; 13 | 14 | &:hover, &:focus{ 15 | z-index: 2; 16 | width: fit-content !important; 17 | max-width: 600px; 18 | text-overflow: ellipsis; 19 | white-space: nowrap; 20 | overflow: hidden; 21 | outline-width: 0px; 22 | } 23 | 24 | &:focus{ 25 | border: 1px solid #0071ff; 26 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 27 | outline-width: 0px; 28 | border-radius: .25rem; 29 | } 30 | 31 | & > div{ 32 | text-align: initial; 33 | position: relative; 34 | color: #303030; 35 | background-color: #f0f0f0; 36 | margin-right: 2px; 37 | margin-left: 2px; 38 | height: 100%; 39 | border-radius: .25rem; 40 | transition: all 0.3s ease 0s; 41 | 42 | 43 | &:hover, &:focus{ 44 | cursor: pointer; 45 | border: 1px solid #0071ff; 46 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 47 | outline-width: 0px; 48 | } 49 | } 50 | 51 | & > div > div{ 52 | display: flex; 53 | flex-direction: row; 54 | flex-wrap: nowrap; 55 | height: 100%; 56 | } 57 | ` 58 | const BodyData = styled.div` 59 | padding: 1rem 1.5rem; 60 | height: 100%; 61 | 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | ` 66 | 67 | const Title = styled.div` 68 | margin: 0; 69 | font-size: 1rem; 70 | font-weight: 500; 71 | line-height: 1.125rem; 72 | 73 | text-overflow: ellipsis; 74 | white-space: nowrap; 75 | overflow: hidden; 76 | ` 77 | 78 | const Description = styled.div` 79 | padding-top: .25rem; 80 | font-size: .875rem; 81 | max-width: 500px; 82 | flex-wrap: nowrap!important; 83 | 84 | text-overflow: ellipsis; 85 | white-space: nowrap; 86 | overflow: hidden; 87 | ` 88 | 89 | const Time = styled.div` 90 | padding-top: .75rem; 91 | font-size: .875rem; 92 | 93 | text-overflow: ellipsis; 94 | white-space: nowrap; 95 | overflow: hidden; 96 | ` 97 | const ProgressBar = styled.div` 98 | position: absolute; 99 | top: 0; 100 | bottom: 0; 101 | left: 0; 102 | right: 0; 103 | ` 104 | 105 | const Progress = styled.div` 106 | background: repeating-linear-gradient(45deg,rgba(0,0,0,.5),rgba(0,0,0,.5) 1px,#b4c6df 1px,#b4c6df 7px); 107 | opacity: .2; 108 | height: 100%; 109 | ` 110 | 111 | 112 | const ProgrammeFake = ({start, stop, dayTime, chId}) => { 113 | const h24Format = useSelector(state => state.h24); 114 | /* 115 | length = each minute is 0.25px; 116 | start = each minute is 2.73px 117 | */ 118 | const lengthStyle = ((stop-start)/3600000)*17; 119 | const startStyle = -2+((start-dayTime)/3600000)*17; 120 | const Spacing = { 121 | minWidth : lengthStyle + "rem", 122 | width : lengthStyle + "rem", 123 | transform: `translateX(${startStyle}rem)` 124 | } 125 | 126 | 127 | return ( 128 | 129 |
    130 |
    131 | 132 | No programme available 133 | 134 | 135 | 136 | 137 | 138 | 139 |
    140 |
    141 |
    142 | ) 143 | } 144 | 145 | export default ProgrammeFake 146 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/TimeBar/TimeLine.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import {convertRemToPixels} from "../../../other/convert-rem" 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | flex-flow: row; 8 | flex-wrap: nowrap!important; 9 | transform: translateY(0px); 10 | ` 11 | 12 | const ArrowContainer = styled.div` 13 | position: sticky; 14 | top: 0; 15 | z-index: 3; 16 | display: inline-block; 17 | 18 | & > div{ 19 | justify-content: flex-end; 20 | background: linear-gradient(to right,#fff 0,#fff 92%,rgba(255,255,255,0) 100%); 21 | display: flex; 22 | align-items: center; 23 | height: 4.5rem; 24 | } 25 | 26 | ` 27 | const Arrow = styled.button` 28 | border: none!important; 29 | cursor: pointer; 30 | min-width: 0!important; 31 | background-color: #fff; 32 | box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%); 33 | border-radius: 1.375rem; 34 | width: 2.5rem; 35 | height: 2.5rem; 36 | user-select: none; 37 | -webkit-tap-highlight-color: transparent; 38 | -webkit-appearance: none; 39 | 40 | &:focus{ 41 | outline-width: 0px; 42 | } 43 | ` 44 | 45 | const TimeLineDiv = styled.div` 46 | width: 6760px; 47 | display: inline-block; 48 | background: #fff; 49 | box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%); 50 | 51 | & > div{ 52 | height: 4.5rem; 53 | display: flex; 54 | align-items: center; 55 | position: relative; 56 | } 57 | 58 | & > div > button{ 59 | position: absolute; 60 | left: 0; 61 | width: 1rem; 62 | white-space: nowrap; 63 | background: 0 0; 64 | border: none; 65 | outline: 0; 66 | padding: .4375rem 1.4375rem; 67 | font-size: .875rem; 68 | -webkit-tap-highlight-color: transparent; 69 | user-select: none; 70 | text-transform: none; 71 | } 72 | 73 | ` 74 | 75 | const TimeLine = ({h24,scrollBtn}) => { 76 | let hours = []; 77 | if(h24 === true){ 78 | for(let i = 0; i < 23; i++){ 79 | let h= i+1<10 ? "0"+(i+1) : i+1; 80 | hours.push({text:`${h}:00`,margin: 12 + (17*i)}); 81 | } 82 | }else{ 83 | let counter = 0; 84 | for(let i = 0; i < 12; i++){ 85 | let h = i; 86 | if(h===0) 87 | h=12; 88 | else h = h<10 ? "0"+i : i; 89 | hours.push({text:`${h}:00 AM`,margin: 12 + (17*counter)}); 90 | counter++; 91 | } 92 | for(let i = 0; i < 12; i++){ 93 | let h = i; 94 | if(h===0) 95 | h=12; 96 | else h = h<10 ? "0"+i : i; 97 | hours.push({text:`${h}:00 PM`,margin: 12 + (17*counter)}); 98 | counter++; 99 | } 100 | } 101 | 102 | 103 | return ( 104 | 105 | scrollBtn(-convertRemToPixels(30))}> 106 |
    107 | 108 |
    109 |
    110 | 111 |
    112 | {hours.map((x,id)=>( 113 | 116 | ))} 117 |
    118 |
    119 | scrollBtn(convertRemToPixels(30))}> 120 |
    121 | 122 |
    123 |
    124 |
    125 | ) 126 | } 127 | 128 | export default TimeLine 129 | -------------------------------------------------------------------------------- /src/other/auth.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, createContext } from "react"; 2 | import {Route,Redirect} from "react-router-dom" 3 | import * as axios from "./axios"; 4 | import {setInfo} from "./user_info" 5 | import Cookies from 'js-cookie' 6 | import {initDb} from "./local-db" 7 | 8 | const authContext = createContext(); 9 | 10 | export function ProvideAuth({ children }) { 11 | const auth = useProvideAuth(); 12 | return {children}; 13 | } 14 | 15 | export const useAuth = () => { 16 | return useContext(authContext); 17 | }; 18 | 19 | export function useProvideAuth() { 20 | const [auth, setAuth] = useState(0); 21 | 22 | const signin = (dns, username, password, successFallback, failFallback) => { 23 | if(dns) 24 | axios.setDns(dns); 25 | 26 | axios.post("player_api.php",{ 27 | username: username.trim(), 28 | password: password.trim(), 29 | }).then(result => { 30 | if(result){ 31 | if(result.data) 32 | result = result.data 33 | else if(result.response && result.response.data) 34 | result = result.response.data 35 | } 36 | if(result && result.user_info){ 37 | if(result.iptveditor) 38 | axios.setDns(`${process.env.REACT_APP_IPTVEDITOR_API}webplayer`); 39 | if(result.user_info.auth === 0) 40 | failFallback("No account found","No account found with inserted credentials." + (window.location.host.includes("iptveditor.com") ? "
    To login use your IPTVEditor's playlist username and password, not your email." : "")); 41 | else if(result.user_info.auth){ 42 | if(result.user_info.status !== "Active") 43 | failFallback("Account expired",`Account expired on ${new Date(parseInt(result.user_info.exp_date+"000")).toGMTString()}`); 44 | else { 45 | setAuth(1); 46 | setInfo(result.user_info, result.server_info); 47 | initDb(); 48 | successFallback && (successFallback()); 49 | 50 | } 51 | } 52 | }else if(result.title){ 53 | failFallback && (failFallback(result.title,result.body)); 54 | }else{ 55 | failFallback && (failFallback("Server error","Server didn't generated any reply. Please try later #2")); 56 | } 57 | }).catch(err => { 58 | console.log(err); 59 | if(err && err.response && err.response.data && err.response.data.user_info && err.response.data.user_info.auth === 0) 60 | failFallback && (failFallback("No account found","No account found with inserted credentials." + (window.location.host.includes("iptveditor.com") ? "
    To login use your IPTVEditor's playlist username and password, not your email." : ""))); 61 | else failFallback && (failFallback("Server error","Server didn't generated any reply. Please try later #1")); 62 | }) 63 | }; 64 | 65 | const authLogin = (fallback) =>{ 66 | 67 | const dns = window.dns.length === 0 && (Cookies.get("dns")) ; 68 | const username = Cookies.get("username"); 69 | const password = Cookies.get("password"); 70 | 71 | if(username && password) 72 | signin(dns,username,password,fallback); 73 | } 74 | 75 | const signout = (action) => { 76 | setAuth(null); 77 | action && (action()); 78 | }; 79 | 80 | const isAuth = () => { 81 | return !!auth; 82 | } 83 | 84 | return { 85 | signin, 86 | signout, 87 | isAuth, 88 | authLogin 89 | }; 90 | } 91 | 92 | 93 | export function PrivateRoute({ children, ...rest }) { 94 | let auth = useAuth(); 95 | return ( 96 | 99 | auth.isAuth() ? ( 100 | children 101 | ) : ( 102 | 108 | ) 109 | } 110 | /> 111 | ); 112 | } -------------------------------------------------------------------------------- /src/components/Series/Episode.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from "styled-components" 3 | 4 | const Container = styled.div` 5 | min-height: 2rem; 6 | height: 6.5rem; 7 | padding: 0.6rem; 8 | border-radius: .2rem; 9 | border: 0.1rem solid transparent; 10 | 11 | border-top-color: black; 12 | border-down-color: black; 13 | 14 | cursor:pointer; 15 | 16 | transition: background-color 0.3s ease, border-color 0.3s ease; 17 | 18 | &:hover{ 19 | background-color: #333; 20 | border-color: white; 21 | } 22 | 23 | & .playbtn{ 24 | opacity: 0; 25 | } 26 | 27 | &:hover .playbtn{ 28 | opacity: 1; 29 | } 30 | 31 | & *{ 32 | cursor:pointer; 33 | } 34 | ` 35 | 36 | const Number = styled.div` 37 | text-align:right; 38 | ` 39 | 40 | const Image = styled.img` 41 | max-width: 100%; 42 | display: block; 43 | ` 44 | 45 | const PlayButton = styled.div` 46 | position: absolute; 47 | display: flex; 48 | place-content: center; 49 | align-items: center; 50 | height: 100%; 51 | width: calc(100% - 1.5rem); 52 | background-color: #00000033; 53 | transition: opacity 0.3s ease; 54 | 55 | ` 56 | 57 | const TitleContainer = styled.div` 58 | display: inline; 59 | align-items: center; 60 | 61 | & > * { 62 | width: 100%; 63 | height: 100%; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | display: -webkit-box; 67 | -webkit-box-orient: vertical; 68 | } 69 | 70 | & > h5{ 71 | -webkit-line-clamp: 1; 72 | } 73 | 74 | & > label{ 75 | -webkit-line-clamp: 2; 76 | font-weight: 300; 77 | } 78 | ` 79 | 80 | const Bar = styled.div` 81 | position: absolute; 82 | display: flex; 83 | height: 4%; 84 | bottom: 0; 85 | width: calc(100% - 1.9rem); 86 | background-color: var(--first-color); 87 | 88 | & > div{ 89 | height:100%; 90 | background-color : var(--second-color); 91 | } 92 | ` 93 | 94 | const DownloadIcon = styled.a` 95 | transition: font-size 0.2s ease; 96 | font-size: 1rem; 97 | cursor: pointer; 98 | color: white; 99 | &:hover{ 100 | font-size: 1.4rem; 101 | color:white; 102 | } 103 | ` 104 | 105 | const Episode = ({episode, image, duration, title, description, selected, playEpisode, percentage, url}) => { 106 | return ( 107 | playEpisode(episode)}> 108 |
    109 | {episode} 110 |
    111 |
    112 | 113 | {selected && percentage > 3 && percentage < 95 && ( 114 | 115 |
    116 | 117 | )} 118 | 119 | 120 |
    121 | 122 |
    123 | {title} 124 |
    125 | {description && ( 126 | 129 | )} 130 |
    131 |
    132 | {secondsToHms(duration)} 133 |
    134 |
    135 | 136 | 137 | 138 |
    139 | 140 | ) 141 | } 142 | 143 | export default Episode 144 | 145 | const secondsToHms = d => { 146 | if(!d) 147 | return ""; 148 | d = parseInt(d); 149 | const h = Math.floor(d / 3600); 150 | const m = Math.floor((d % 3600) / 60); 151 | const s = Math.floor((d % 3600) % 60); 152 | 153 | if (h) { 154 | return `${h}:${m<10 ? "0"+m : m}:${s<10 ? "0"+s : s}`; 155 | } 156 | 157 | return `${m}:${s<10 ? "0"+s : s}`; 158 | }; 159 | -------------------------------------------------------------------------------- /src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react" 2 | import styled from 'styled-components' 3 | import { useHistory, useParams, useLocation } from 'react-router'; 4 | import "./Search.css" 5 | 6 | const Container = styled.div` 7 | position:absolute; 8 | width:100%; 9 | height:100%; 10 | top:0; 11 | left:0; 12 | z-index: 5; 13 | transition: all .5s ease; 14 | opacity: 0; 15 | ` 16 | 17 | const Box = styled.form` 18 | position: relative; 19 | width: 50% ; 20 | left: 25% ; 21 | top: 50% ; 22 | display:flex; 23 | ` 24 | 25 | const Input = styled.input` 26 | &:focus{ 27 | color:white; 28 | border-color: var(--second-color); 29 | box-shadow: 0 0 0 0.2rem var(--second-color-shadow); 30 | background-color: var(--first-color); 31 | transition: all .5s ease; 32 | }` 33 | 34 | const ClearButton = styled.button` 35 | color: #fff; 36 | background-color: var(--first-color); 37 | border-color: var(--second-color); 38 | transition: all .5s ease; 39 | margin-right: 0.3rem; 40 | 41 | 42 | &:hover, &:focus{ 43 | background-color: #829196; 44 | color:white; 45 | border-color: #829196; 46 | box-shadow: 0 0 0 0.2rem #82919608; 47 | transition: all .5s ease; 48 | } 49 | ` 50 | 51 | const SearchButton = styled.button` 52 | color: #fff; 53 | background-color: var(--second-color); 54 | transition: all .5s ease; 55 | margin-left: 0.3rem; 56 | 57 | 58 | &:hover, &:focus{ 59 | background-color: #829196; 60 | color:white; 61 | border-color: #829196; 62 | box-shadow: 0 0 0 0.2rem #82919608; 63 | transition: all .5s ease; 64 | } 65 | ` 66 | 67 | const Search = () => { 68 | const query = useQuery(); 69 | const history = useHistory(); 70 | 71 | const [searchValue, setSearchValue] = useState(query.get("search") || ""); 72 | const [startingEffect, setStartingEffect] = useState(); 73 | 74 | const { playingMode, category } = useParams(); 75 | 76 | useEffect(() => { 77 | setTimeout(()=> setStartingEffect("blur-search"),1) 78 | }, []) 79 | 80 | const handleSubmit = (evt) => { 81 | evt.preventDefault(); 82 | alert(`Submitting Name ${searchValue}`) 83 | } 84 | 85 | 86 | 87 | const close = (e) => { 88 | e.preventDefault(); 89 | if(!searchValue){ 90 | clear(); 91 | return; 92 | } 93 | setStartingEffect(); 94 | setTimeout(()=> { 95 | history.replace(category ? `/${playingMode}/category/${category}/?search=${searchValue}` : `/${playingMode}/?search=${searchValue}`); 96 | },300) 97 | } 98 | 99 | const clear = () => { 100 | setStartingEffect(); 101 | setTimeout(()=> { 102 | history.replace(category ? `/${playingMode}/category/${category}` : `/${playingMode}/`); 103 | },300) 104 | } 105 | 106 | return ( 107 | <> 108 | 109 | 110 | 111 | 112 | 113 | < Input className = "form-control" 114 | type = "text" 115 | spellCheck = {false} 116 | placeholder = "Search stream..." 117 | onChange = {(e) => setSearchValue(e.target.value)} 118 | value = {searchValue} 119 | /> 120 | 121 | 122 | 123 | 124 | 125 | {/*showKeyboard !== false && isMag && ()*/} 126 | 127 | ) 128 | } 129 | 130 | const useQuery = () => { 131 | return new URLSearchParams(useLocation().search); 132 | } 133 | 134 | export default Search 135 | -------------------------------------------------------------------------------- /src/components/Live/Channels.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect,useRef, useState} from 'react' 2 | import styled from "styled-components" 3 | import {useSelector, useDispatch} from "react-redux" 4 | import Channel from "./Channel" 5 | import {setPlayingChannel} from "../../actions/playingChannel" 6 | import { FixedSizeList as List } from "react-window"; 7 | import {convertRemToPixels,convertVhToPixels} from "../../other/convert-rem" 8 | import {useHistory,useLocation} from "react-router-dom"; 9 | import Popup from "../Popup/Popup" 10 | 11 | const ChannelsList = styled.ul` 12 | height: calc(95vh - 3rem); 13 | padding: .8rem .6rem; 14 | background-color: var(--first-color); 15 | list-style-type: none; 16 | border-left: 1px solid #000; 17 | transform: translateY(-1px); 18 | box-shadow: 0 7px 14px rgb(50 50 93 / 10%), 0 3px 6px rgb(0 0 0 / 8%); 19 | &:focus{ 20 | outline-width: 0px; 21 | } 22 | ` 23 | 24 | const Channels = ({playlist}) => { 25 | const playingChannel = useSelector(state => state.playingCh); 26 | 27 | const query = useQuery(); 28 | const searchText = query.get("search"); 29 | 30 | const [showPopup, setShowPopup] = useState(false); 31 | const [channelsPlaylist, setChannelsPlaylist] = useState(playlist); 32 | 33 | 34 | const dispatch = useDispatch() 35 | const history = useHistory(); 36 | const location = useLocation(); 37 | const listRef = useRef(); 38 | 39 | 40 | useEffect(() => { 41 | if(playingChannel){ 42 | setTimeout(()=>focusChannel(),100) 43 | } 44 | }, [playingChannel]); 45 | 46 | useEffect(() => { 47 | setChannelsPlaylist(playlist) 48 | setTimeout(()=>focusChannel(),100) 49 | }, [playlist]); 50 | 51 | 52 | useEffect(() => { 53 | const searched = searchText ? playlist.filter(x=> x.name.toLowerCase().includes(searchText.toLowerCase())) : playlist 54 | if(searched.length>0){ 55 | setChannelsPlaylist(searched) 56 | setTimeout(() => focusChannel(), 100); 57 | }else if(searchText) 58 | setShowPopup(1) 59 | else history.push("/live/category/") 60 | }, [searchText]); 61 | 62 | const focusChannel = () => { 63 | if(document.getElementById("selectedCh")) 64 | document.getElementById("selectedCh").focus() 65 | else if(document.querySelector(".channel")) 66 | document.querySelector(".channel").focus() 67 | } 68 | 69 | 70 | const Row = ({ index, style }) => ( 71 | { 78 | dispatch(setPlayingChannel(channelsPlaylist[index])) 79 | }} 80 | virtualScrollStyle={style} 81 | selected={playingChannel && playingChannel.stream_id === channelsPlaylist[index].stream_id} /> 82 | ); 83 | 84 | return ( 85 | <> 86 | 87 | 95 | {Row} 96 | 97 | 98 | {showPopup && { 99 | setShowPopup(0); 100 | focusChannel(); 101 | history.replace(location.pathname.split("?")[0]) 102 | }}/>} 103 | 104 | ) 105 | } 106 | 107 | const useQuery = () => { 108 | return new URLSearchParams(useLocation().search); 109 | } 110 | 111 | export default Channels 112 | -------------------------------------------------------------------------------- /src/components/Live/MainLive.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState} from 'react' 2 | import Player from "../Player/Player" 3 | import EpgListing from "./EpgListing" 4 | import Channels from "./Channels" 5 | 6 | import styled from "styled-components" 7 | import {useSelector, useDispatch} from "react-redux" 8 | 9 | import {setPlaylist} from "../../actions/set-Playlist" 10 | import {setGroupList} from "../../actions/set-Group" 11 | 12 | import {resetMemory, setGroup} from "../../other/last-opened-mode" 13 | 14 | import {loadGroup,loadPlaylist} from "../../other/load-playlist" 15 | import {useParams, useHistory} from "react-router-dom"; 16 | 17 | import Popup from "../Popup/Popup" 18 | 19 | import DB from "../../other/local-db" 20 | 21 | const Main = styled.div` 22 | padding: 3vh 1vw; 23 | background-color: var(--first-color); 24 | height: calc(100vh - 3rem); 25 | z-index:1; 26 | transition: filter 0.5s ease; 27 | 28 | ` 29 | 30 | const ChannelTitle = styled.h5` 31 | color:white; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | ` 35 | 36 | 37 | const MainLive = () => { 38 | const [blurBackground, setBlurBackground] = useState() 39 | 40 | const playingChannel = useSelector(state => state.playingCh); 41 | const playlist = useSelector(state => state.playlist); 42 | const dispatch = useDispatch() 43 | const history = useHistory() 44 | 45 | const { category } = useParams(); 46 | const [showPopup, setShowPopup] = useState(false); 47 | 48 | useEffect(()=>{ 49 | const fun = async() => { 50 | setBlurBackground(((isNaN(category) || category === undefined) || history.location.pathname.includes("menu")) ? {filter:"blur(.5rem)", pointerEvents: "none"} : {}); 51 | 52 | if(category != undefined && category != 0){ 53 | let chs = await loadPlaylist("live",category) 54 | chs = chs || []; 55 | if(category==="fav") 56 | chs = chs.filter(x=> DB.findOne("live",x.stream_id,true)) 57 | dispatch(setPlaylist(chs)); 58 | if(chs.length === 0) 59 | setShowPopup(1) 60 | }else if(resetMemory("live")){ 61 | await loadGroup("live").then(gps => { 62 | if(!gps || gps.length===0){ 63 | history.replace("/") 64 | return; 65 | } 66 | gps.unshift({category_name : "Only favorites", category_id:"fav"}) 67 | setGroup(gps[1].category_id) 68 | dispatch(setGroupList(gps)); 69 | history.replace("/live/category/"+gps[1].category_id+"/") 70 | }) 71 | }else history.replace("/live/category/"); 72 | } 73 | fun() 74 | },[dispatch,category]) 75 | 76 | useEffect(() => { 77 | if(history.location) 78 | setBlurBackground(((isNaN(category) && category === undefined) || history.location.pathname.includes("menu")) ? {filter:"blur(.5rem)", pointerEvents: "none"} : {}); 79 | }, [history.location.pathname, category]) 80 | 81 | 82 | return ( 83 |
    84 |
    85 |
    86 |
    87 | {playingChannel ? playingChannel.name : "No channel selected"} 88 | {} 89 | 90 |
    91 |
    92 | 93 |
    94 |
    95 |
    96 | {showPopup && { 97 | setBlurBackground({}) 98 | setShowPopup(0); 99 | history.replace("/live/category/") 100 | }}/>} 101 | {/*window.gSTB && ()*/} 102 |
    103 | ) 104 | } 105 | 106 | export default MainLive 107 | -------------------------------------------------------------------------------- /public/proxy.php: -------------------------------------------------------------------------------- 1 | 0) { 64 | // Limit POST data size to prevent memory exhaustion 65 | $max_post_size = 1024 * 100; // 100KB 66 | $post_json = json_encode($_POST); 67 | 68 | if (strlen($post_json) > $max_post_size) { 69 | http_response_code(413); 70 | die('Request too large'); 71 | } 72 | 73 | $post_data = $post_json; 74 | } 75 | 76 | // Set timeout and configure stream context with security options 77 | $timeout = 30; // 30 seconds timeout 78 | 79 | try { 80 | if ($post_data !== null) { 81 | $options = array( 82 | 'http' => array( 83 | 'method' => 'POST', 84 | 'content' => $post_data, 85 | 'header' => "Content-Type: application/json\r\n" . 86 | "Accept: application/json\r\n" . 87 | "User-Agent: StreamityProxy/1.0\r\n", 88 | 'timeout' => $timeout, 89 | 'ignore_errors' => false, 90 | 'follow_location' => 0 // Prevent redirect attacks 91 | ) 92 | ); 93 | $context = stream_context_create($options); 94 | $result = @file_get_contents($url, false, $context); 95 | } else { 96 | $options = array( 97 | 'http' => array( 98 | 'method' => 'GET', 99 | 'timeout' => $timeout, 100 | 'ignore_errors' => false, 101 | 'follow_location' => 0, // Prevent redirect attacks 102 | 'header' => "User-Agent: StreamityProxy/1.0\r\n" 103 | ) 104 | ); 105 | $context = stream_context_create($options); 106 | $result = @file_get_contents($url, false, $context); 107 | } 108 | 109 | if ($result === false) { 110 | http_response_code(502); 111 | die('Failed to fetch content'); 112 | } 113 | 114 | // Set appropriate content type if available 115 | if (isset($http_response_header)) { 116 | foreach ($http_response_header as $header) { 117 | if (stripos($header, 'Content-Type:') === 0) { 118 | header($header); 119 | break; 120 | } 121 | } 122 | } 123 | 124 | echo $result; 125 | 126 | } catch (Exception $e) { 127 | http_response_code(500); 128 | die('Internal server error'); 129 | } 130 | 131 | ?> -------------------------------------------------------------------------------- /src/components/Live/ChannelEpg.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled from "styled-components" 3 | import {useSelector} from "react-redux" 4 | import ChannelEpgBar from "./ChannelEpgBar" 5 | import DB from "../../other/local-db" 6 | 7 | import {getSingleEpgNow, downloadEpgData} from "../../other/epg-database" 8 | 9 | const dateFormat = require("dateformat"); 10 | 11 | const HeaderChannel = styled.div` 12 | padding-bottom: .4rem!important; 13 | line-height: 1!important; 14 | font-size: 0.65rem; 15 | font-weight: 700; 16 | ` 17 | 18 | const BodyChannel = styled.div` 19 | text-transform: capitalize; 20 | overflow: hidden; 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | ` 24 | const Title = styled.label` 25 | font-size: 1rem; 26 | max-width: 100%; 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis;` 30 | 31 | const TitleEpg = styled.label` 32 | padding-left: 2%; 33 | font-size: 0.9rem; 34 | font-weight: 100; 35 | max-width: 70%; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis;` 39 | 40 | /*const PlayingButton = styled.i` 41 | font-size: 2.2rem; 42 | color: #fff; 43 | margin-left:2px; 44 | `*/ 45 | 46 | const Favorite = styled.div` 47 | width: 1rem; 48 | display: flex; 49 | align-content: center; 50 | align-self: center; 51 | z-index:10; 52 | 53 | & > i { 54 | margin-left:2px; 55 | } 56 | ` 57 | 58 | const ChannelEpg = ({chId, Name, Epg, Shift, isPlaying}) => { 59 | const h24Format = useSelector(state => state.h24); 60 | const timer = useSelector(state => state.timer60); 61 | const [epgNow, setEpgNow] = useState(false); 62 | const [favorite, setFavorite] = useState(null) 63 | 64 | useEffect(() => { 65 | setFavorite(!!DB.findOne("live",chId,true)) 66 | if(!Epg) 67 | return; 68 | async function fetchData() { 69 | await downloadEpgData(chId, Epg, 1, Shift); 70 | const newEpg = getSingleEpgNow(Epg,Shift); 71 | if(!newEpg) 72 | setEpgNow(false); 73 | else if(newEpg && (epgNow === false || epgNow.start !== newEpg.start)){ 74 | setEpgNow({...newEpg}); 75 | } 76 | } 77 | fetchData(); 78 | }, []); 79 | 80 | useEffect(() => { 81 | setFavorite(!!DB.findOne("live",chId,true)) 82 | }, [isPlaying]) 83 | 84 | useEffect(() => { 85 | const newEpg = getSingleEpgNow(Epg,Shift); 86 | if(!newEpg) 87 | setEpgNow(false); 88 | else if(newEpg && (epgNow === false || epgNow.start !== newEpg.start)){ 89 | setEpgNow({...newEpg}); 90 | } 91 | }, [timer]); 92 | 93 | const setFavoriteGlob = (event) =>{ 94 | event.stopPropagation(); 95 | 96 | if(!!favorite) 97 | DB.del("live",chId,true) 98 | else DB.set("live",chId,{id: chId}, true) 99 | setFavorite(!favorite); 100 | } 101 | 102 | return ( 103 | <> 104 |
    105 | 106 | {epgNow !== false && `${dateFormat(new Date(epgNow.start), h24Format)} - ${dateFormat(new Date(epgNow.end), h24Format)}`} 107 | 108 | 109 | <b>{Name}</b> 110 | {epgNow.title} 111 | 112 |
    113 | {/*isPlaying ? (
    114 | 115 |
    )*/} 116 | 117 | 118 | 119 |
    120 | {epgNow !== false ? 121 | () 122 | : 123 | 124 | } 125 |
    126 | 127 | ) 128 | } 129 | 130 | export default ChannelEpg 131 | 132 | -------------------------------------------------------------------------------- /src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {Link,useLocation, useParams} from 'react-router-dom' 3 | import styled from "styled-components" 4 | import {useSelector} from "react-redux" 5 | import {useAuth} from "../other/auth" 6 | 7 | 8 | const Nav = styled.nav` 9 | height: 3rem; 10 | background-color: var(--first-color); 11 | border-bottom: 1px solid #000; 12 | `; 13 | 14 | const ContainerButton = styled.div` 15 | position:absolute; 16 | right:2rem; 17 | top:.5rem; 18 | display: flex; 19 | 20 | & > a{ 21 | color: #fff; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: 1rem; 25 | text-transform: uppercase; 26 | display: flex; 27 | background: #829196; 28 | height: 2rem; 29 | width: 2rem; 30 | border-radius: 50%; 31 | } 32 | 33 | ` 34 | 35 | const CategoryButton = styled(Link)` 36 | margin-right: 1rem; 37 | &:hover { 38 | width: 11rem; 39 | -webkit-transition: width 1s; 40 | transition: width .5s; 41 | overflow:hidden; 42 | white-space: nowrap; 43 | border-radius:5px; 44 | text-decoration: none; 45 | color: #fff; 46 | padding-left:0.3rem; 47 | padding-right:0.3rem; 48 | } 49 | 50 | &:hover::after { 51 | padding-left:0.3rem; 52 | padding-right:0.3rem; 53 | content: 'Select category'; 54 | } 55 | ` 56 | 57 | const EPGButton = styled(Link)` 58 | margin-right: 1rem; 59 | &:hover { 60 | width: 8rem; 61 | -webkit-transition: width 1s; 62 | transition: width .5s; 63 | overflow:hidden; 64 | white-space: nowrap; 65 | border-radius:5px; 66 | text-decoration: none; 67 | color: #fff; 68 | padding-left:0.3rem; 69 | padding-right:0.3rem; 70 | } 71 | 72 | &:hover::after { 73 | padding-left:0.3rem; 74 | padding-right:0.3rem; 75 | content: 'TV Guide'; 76 | } 77 | ` 78 | 79 | const SearchButton = styled(Link)` 80 | margin-right: 1rem; 81 | &:hover { 82 | width: 8rem; 83 | -webkit-transition: width 1s; 84 | transition: width .5s; 85 | overflow:hidden; 86 | white-space: nowrap; 87 | border-radius:5px; 88 | text-decoration: none; 89 | color: #fff; 90 | padding-left:0.3rem; 91 | padding-right:0.3rem; 92 | } 93 | 94 | &:hover::after { 95 | padding-left:0.3rem; 96 | padding-right:0.3rem; 97 | content: 'Search...'; 98 | } 99 | ` 100 | 101 | const NavButton = styled(Link)` 102 | margin-right: 1rem; 103 | &:hover { 104 | width: 10rem; 105 | -webkit-transition: width 1s; 106 | transition: width .5s; 107 | overflow:hidden; 108 | white-space: nowrap; 109 | border-radius:5px; 110 | text-decoration: none; 111 | color: #fff; 112 | padding-left:0.3rem; 113 | padding-right:0.3rem; 114 | } 115 | 116 | &:hover::after { 117 | padding-left:0.3rem; 118 | padding-right:0.3rem; 119 | content: 'Open menu'; 120 | } 121 | ` 122 | 123 | const NavBar = () => { 124 | const fullScreen = useSelector(state => state.fullScreen); 125 | const location = useLocation(); 126 | let auth = useAuth(); 127 | 128 | const { playingMode } = useParams(); 129 | 130 | return ( 131 | 155 | ) 156 | } 157 | 158 | export default NavBar 159 | -------------------------------------------------------------------------------- /src/components/Series/PlayerSeries.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import ReactNetflixPlayer from "../../other/Player-github/player-github" 3 | import styled from 'styled-components' 4 | import {useParams,useHistory} from "react-router-dom"; 5 | import {optimizeName} from "../../other/vod-series-name-optimizer" 6 | import {useSelector} from "react-redux" 7 | import DB from "../../other/local-db" 8 | 9 | const Container = styled.div` 10 | position:absolute; 11 | width: 100%; 12 | height: 100%; 13 | top: 0; 14 | left: 0; 15 | ` 16 | 17 | const PlayerSeries = () => { 18 | const {category,season,episode, stream_id} = useParams(); 19 | const history = useHistory(); 20 | 21 | const tvseries = useSelector(state => state.playlist).find(x=> parseInt(x.series_id) === parseInt(stream_id)).name; 22 | const streamsTemp = useSelector(state => state.playlistEpisodes)/*.map(x => { 23 | return { 24 | ...x, 25 | playing: parseInt(x.episode_num) === parseInt(episode) 26 | } 27 | });*/ 28 | 29 | const [stream, setStream] = useState(); 30 | const [streams, setStreams] = useState([]); 31 | const [streamStat, setStreamStat] = useState() 32 | 33 | useEffect(()=>{ 34 | if(!streamsTemp || streamsTemp.length === 0){ 35 | history.replace(`/series/`); 36 | return; 37 | } 38 | setStreams(streamsTemp.map(x => { 39 | return { 40 | ...x, 41 | playing: parseInt(x.episode_num || x.episode) === parseInt(episode), 42 | id: x.episode_num || x.episode, 43 | name: x.title 44 | } 45 | })) 46 | },[streamsTemp,episode]) 47 | 48 | useEffect(()=>{ 49 | if(streams && streams.length>0){ 50 | const s = streams.find(x=>x.playing===true) 51 | /*if(!s.direct_source) 52 | s.direct_source = generateUrl("series", s.stream, s.container_extension)*/ 53 | 54 | let stat = DB.findOne("series",stream_id); 55 | (!stat || (stat && (parseInt(stat.season) !== parseInt(season) || parseInt(stat.episode) !== parseInt(episode)))) && (stat={id: stream_id, season: season, episode: s.id, start:0, tot:0}) 56 | setStreamStat(stat) 57 | DB.set("series",stream_id, stat) 58 | 59 | setStream(s); 60 | } 61 | },[streams]) 62 | 63 | const setStat = (stat) =>{ 64 | setStreamStat(stat); 65 | DB.set("series",stream_id, stat) 66 | } 67 | 68 | return ( 69 | 70 | history.goBack()} 76 | FullPlayer 77 | autoPlay 78 | startPosition={streamStat ? parseInt(streamStat.start) : 0} 79 | overlayEnabled 80 | autoControllCloseEnabled 81 | Style 82 | primaryColor="var(--second-color)" 83 | secundaryColor="var(--first-color)" 84 | reprodutionList={streams} 85 | playlistTitle = {"Season " + season} 86 | syncDuration = {(duration, percentage) => setStat({...streamStat, start:duration, tot : parseInt(percentage), episode: episode, season: season})} 87 | 88 | dataNext={()=>{ 89 | let next = streams.find(x=>x.episode_num > stream.episode_num); 90 | return { title: next ? `Next: ${next.title}` : ""} 91 | }} 92 | 93 | onClickItemListReproduction={(id, playing) => { 94 | let next = streams.find(x=>x.episode_num === id); 95 | history.replace(`/series/category/${category}/${stream_id}/play/season/${season}/episode/${next.episode_num}/`) 96 | }} 97 | 98 | onEnded={() => { 99 | if(stream){ 100 | let next = streams.find(x=>x.episode_num > stream.episode_num); 101 | if(!next) 102 | return history.back(); 103 | else history.replace(`/series/category/${category}/${stream_id}/play/season/${season}/episode/${next.episode_num}/`) 104 | } 105 | }} 106 | 107 | /> 108 | 109 | ) 110 | } 111 | 112 | export default PlayerSeries 113 | -------------------------------------------------------------------------------- /src/components/Group/Groups.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react" 2 | import "./Groups.css" 3 | import styled from "styled-components" 4 | import {useSelector} from "react-redux" 5 | import {useHistory, useParams} from "react-router-dom"; 6 | import {getGroup} from "../../other/last-opened-mode" 7 | 8 | 9 | 10 | const Input = styled.input` 11 | &:focus{ 12 | border-color: var(--second-color); 13 | box-shadow: 0 0 0 0.2rem var(--second-color-shadow); 14 | transition: all .5s ease; 15 | }` 16 | 17 | 18 | const Groups = () => { 19 | const [navbar,setNavbar] = useState(false) 20 | const [searchValue, setSearchValue] = useState(""); 21 | 22 | const history = useHistory(); 23 | const { playingMode, category } = useParams(); 24 | 25 | const groupsTemp = useSelector(state => state.groupsList); 26 | const [groups, setGroups] = useState(groupsTemp) 27 | 28 | const closeBar = () => { 29 | setNavbar(false) 30 | setTimeout(()=> history.goBack(),500); 31 | } 32 | 33 | useEffect(()=>{ 34 | setNavbar(isNaN(category)) 35 | },[category]) 36 | 37 | const searchGroup = (text) =>{ 38 | setSearchValue(text); 39 | setGroups(text ? groupsTemp.filter(x=> x.category_name.toLowerCase().includes(text.toLowerCase())) : groupsTemp) 40 | } 41 | 42 | useEffect(()=>{ 43 | setSearchValue() 44 | setGroups(groups) 45 | },[groupsTemp]) 46 | 47 | const selectGroup = (gp) =>{ 48 | setNavbar(false); 49 | if(gp===-1 && playingMode !== "live") 50 | setTimeout(()=>history.replace(`/${playingMode}/`), 500); 51 | else{ 52 | setGroups(groups[gp].category_id) 53 | setTimeout(()=>history.replace(`/${playingMode}/category/${groups[gp].category_id}/`), 500); 54 | } 55 | } 56 | 57 | 58 | useEffect(() => { 59 | if(!groups || groups.length===0) 60 | history.replace("/") 61 | }, []); 62 | 63 | return ( 64 | <> 65 |
    66 |
    67 |
    68 |
    69 |
    70 | < Input className = "form-control" 71 | type = "text" 72 | spellCheck = {false} 73 | placeholder = "Search category..." 74 | onChange = {(e) => searchGroup(e.target.value)} 75 | value = {searchValue} 76 | /> 77 |
    78 |
    79 | 80 |
    81 |
    82 | 83 | 84 |
      85 | {playingMode !== "live" && ( 86 |
    • 87 | 95 |
    • 96 | )} 97 | {groups && Array.isArray(groups) && (groups.map((gp,id) => ( 98 |
    • 99 | 107 |
    • 108 | )))} 109 |
    110 |
    111 |
    112 |
    113 | 114 | ) 115 | } 116 | 117 | export default Groups 118 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/ProgrammeVIRTUAL.js.BK: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useState, useEffect } from 'react' 4 | import {useSelector, useDispatch} from "react-redux" 5 | import Popup from "./Popup" 6 | import {setEpgPopup} from "../../actions/epgPopup" 7 | 8 | const dateFormat = require("dateformat"); 9 | 10 | const Container = styled.li` 11 | position: absolute; 12 | left: 0; 13 | height: 100%; 14 | background-color: #fff; 15 | transition: all 0.3s ease 0s; 16 | 17 | &:hover, &:focus{ 18 | z-index: 2; 19 | width: fit-content !important; 20 | max-width: 600px; 21 | text-overflow: ellipsis; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | outline-width: 0px; 25 | } 26 | 27 | &:focus{ 28 | border: 1px solid #0071ff; 29 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 30 | outline-width: 0px; 31 | border-radius: .25rem; 32 | } 33 | 34 | & > div{ 35 | text-align: initial; 36 | position: relative; 37 | color: #303030; 38 | background-color: #f0f0f0; 39 | margin-right: 2px; 40 | margin-left: 2px; 41 | height: 100%; 42 | border-radius: .25rem; 43 | transition: all 0.3s ease 0s; 44 | 45 | 46 | &:hover, &:focus{ 47 | cursor: pointer; 48 | border: 1px solid #0071ff; 49 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 50 | outline-width: 0px; 51 | } 52 | } 53 | 54 | & > div > div{ 55 | display: flex; 56 | flex-direction: row; 57 | flex-wrap: nowrap; 58 | height: 100%; 59 | } 60 | ` 61 | const BodyData = styled.div` 62 | padding: 1rem 1.5rem; 63 | height: 100%; 64 | 65 | text-overflow: ellipsis; 66 | white-space: nowrap; 67 | overflow: hidden; 68 | ` 69 | 70 | const Title = styled.div` 71 | margin: 0; 72 | font-size: 1rem; 73 | font-weight: 500; 74 | line-height: 1.125rem; 75 | 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | overflow: hidden; 79 | ` 80 | 81 | const Description = styled.div` 82 | padding-top: .25rem; 83 | font-size: .875rem; 84 | max-width: 500px; 85 | flex-wrap: nowrap!important; 86 | 87 | text-overflow: ellipsis; 88 | white-space: nowrap; 89 | overflow: hidden; 90 | ` 91 | 92 | const Time = styled.div` 93 | padding-top: .75rem; 94 | font-size: .875rem; 95 | 96 | text-overflow: ellipsis; 97 | white-space: nowrap; 98 | overflow: hidden; 99 | ` 100 | const ProgressBar = styled.div` 101 | position: absolute; 102 | top: 0; 103 | bottom: 0; 104 | left: 0; 105 | right: 0; 106 | ` 107 | 108 | const Progress = styled.div` 109 | background: repeating-linear-gradient(45deg,rgba(0,0,0,.5),rgba(0,0,0,.5) 1px,#b4c6df 1px,#b4c6df 7px); 110 | opacity: .2; 111 | height: 100%; 112 | ` 113 | 114 | const isActive = { 115 | color: "#fff", 116 | backgroundColor: "#064497" 117 | } 118 | 119 | const Programme = ({start, stop, title, description, dayTime, style}) => { 120 | const dispatch = useDispatch(); 121 | 122 | const timer = useSelector(state => state.timer60); 123 | const h24Format = useSelector(state => state.h24); 124 | /* 125 | length = each minute is 0.25px; 126 | start = each minute is 2.73px 127 | */ 128 | const [activeLength, setActiveLenght] = useState(stop > timer && start <= timer ? ((timer-start)/(stop-start))*100 : false); 129 | const lengthStyle = ((stop-start)/3600000)*15; 130 | const startStyle = ((start-dayTime)/3600000)*15; 131 | const Spacing = { 132 | minWidth : lengthStyle + "rem", 133 | width : lengthStyle + "rem", 134 | transform: `translateX(${startStyle}rem)` 135 | } 136 | 137 | 138 | useEffect(() => { 139 | if(!(stop > timer && start <= timer)){ 140 | if(activeLength) 141 | setActiveLenght(false); 142 | }else setActiveLenght(((timer-start)/(stop-start))*100) 143 | }, [timer,start,stop,activeLength]); 144 | 145 | 146 | return ( 147 | dispatch(setEpgPopup({title: title, description: description, start:start, stop:stop}))} className={activeLength > 0 ? "isActive" : "noactive"}> 148 |
    0 ? isActive : {}}> 149 |
    150 | 151 | {title} 152 | {description} 153 | 154 | 155 | 156 | 157 | 158 |
    159 |
    160 |
    161 | ) 162 | } 163 | 164 | export default Programme 165 | -------------------------------------------------------------------------------- /src/components/Group-old/Groups.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef} from 'react' 2 | import styled from "styled-components" 3 | import {useSelector,useDispatch} from "react-redux" 4 | import {useHistory, useParams} from "react-router-dom"; 5 | import {setGroup, getGroup} from "../../other/last-opened-mode" 6 | 7 | import "../../other/Transition.css" 8 | 9 | const Background = styled.div` 10 | width:100vw; 11 | height:100vh; 12 | background-color: transparent; 13 | right:0; 14 | top:0; 15 | position: absolute; 16 | z-index:100; 17 | transition: all 1s ease; 18 | ` 19 | 20 | const Container = styled.div` 21 | position: absolute; 22 | height:100vh; 23 | width:75vw; 24 | right:0; 25 | top:0; 26 | transition: all .5s ease; 27 | transform: translateX(75vw); 28 | ` 29 | 30 | const SmokeEffect = styled.div` 31 | position: absolute; 32 | height:100vh; 33 | width:25vw; 34 | background-image: linear-gradient(to left, #212121 0% , transparent); 35 | right:50vw; 36 | top:0; 37 | z-index:11; 38 | ` 39 | 40 | const GroupContainer = styled.div` 41 | position: absolute; 42 | background-color:#212121; 43 | right:0vw; 44 | top:0; 45 | height:100vh; 46 | width:50vw; 47 | z-index:15; 48 | vertical-align: middle; 49 | padding: 1rem 0; 50 | ` 51 | 52 | const GroupList = styled.ul` 53 | right:1rem; 54 | list-style: none; 55 | display: table-cell; 56 | vertical-align: middle; 57 | min-height:calc(100vh - 5rem); 58 | height:calc(100vh - 5rem); 59 | width:45vw; 60 | &:focus{ 61 | outline-width: 0px; 62 | } 63 | ` 64 | 65 | const GroupScroller = styled.div` 66 | height:calc(100vh - 7rem); 67 | padding: 1rem 1.5rem; 68 | overflow:auto; 69 | ` 70 | 71 | const GroupItem = styled.li` 72 | display: block; 73 | color:white; 74 | text-align: end; 75 | font-size: 1.5rem; 76 | border-radius: 5px; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | text-decoration: none; 80 | padding-right: 1rem; 81 | transition: all .15s ease; 82 | 83 | &:hover, &:focus{ 84 | outline-width: 0px; 85 | cursor: pointer; 86 | background-color: #fff; 87 | color: #020d18; 88 | } 89 | ` 90 | 91 | const Title = styled.h1` 92 | color:white; 93 | text-align: center; 94 | ` 95 | 96 | const CloseMessage = styled.h5` 97 | color:white; 98 | text-align: center; 99 | ` 100 | 101 | 102 | const ClosePopup = styled.a` 103 | color:white; 104 | position:absolute; 105 | right:49vw; 106 | margin-bottom: .5rem; 107 | font-weight: 500; 108 | line-height: 1.2; 109 | font-size: 2.5rem; 110 | transition: all .15s ease; 111 | 112 | &:hover{ 113 | color: #020d18; 114 | cursor: pointer; 115 | } 116 | ` 117 | 118 | let timeout; 119 | const Groups = () => { 120 | const gpContainerRef = useRef(null) 121 | const backGroundRef = useRef(null) 122 | const dispatch = useDispatch() 123 | const history = useHistory(); 124 | const { playingMode } = useParams(); 125 | const category = getGroup(); 126 | 127 | const groups = useSelector(state => state.groupsList); 128 | 129 | 130 | const selectGroup = (gp) =>{ 131 | setGroup(groups[gp].category_id) 132 | history.replace(`/${playingMode}/category/${groups[gp].category_id}/`); 133 | } 134 | 135 | useEffect(() => { 136 | if(!groups || groups.length==0) 137 | history.replace("/") 138 | setTimeout(()=>{ 139 | backGroundRef.current && (backGroundRef.current.style.backgroundColor="#00000085"); 140 | gpContainerRef.current && (gpContainerRef.current.style.transform="translateX(0)"); 141 | }, 100); 142 | }, []); 143 | 144 | const closePopup = () =>{ 145 | backGroundRef.current.style.backgroundColor="transparent"; 146 | gpContainerRef.current.style.transform="translateX(75vw)"; 147 | setTimeout(()=>{ 148 | history.replace(`/${playingMode}/category/${category}/`); 149 | }, 150); 150 | } 151 | 152 | return ( 153 | 154 | 155 | 156 | 157 | 158 | Choose a Category 159 | 160 | 161 | {groups.map((gp,id) => ( 162 | selectGroup(id)} 166 | >{gp.category_name} 167 | ))} 168 | 169 | 170 | {/*window.gSTB && (Press to close)*/} 171 | 172 | 173 | 174 | 175 | ) 176 | } 177 | 178 | export default Groups 179 | -------------------------------------------------------------------------------- /src/components/Vod/RowItem.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import styled from 'styled-components' 3 | import {optimizeName} from "../../other/vod-series-name-optimizer" 4 | import PopupHover from "./PopupHover" 5 | import {Link} from "react-router-dom"; 6 | import DB from "../../other/local-db" 7 | import {generateUrl} from "../../other/generate-url" 8 | 9 | const Li = styled(Link)` 10 | height: calc(15vw * 1.5); 11 | max-height: 60vh; 12 | text-align: center; 13 | flex: 0 0 15.266667%; 14 | max-width: 15.266667%; 15 | padding-left: .4rem; 16 | padding-right: .3rem; 17 | cursor: pointer; 18 | outline-width: 0; 19 | text-decoration: none !important; 20 | 21 | &:focus > div { 22 | box-shadow: rgb(255 0 0 / 75%) 0px 3px 10px; 23 | outline-width: 0; 24 | } 25 | ` 26 | 27 | const ImgContainer = styled.div` 28 | height: calc(100% - 1.5rem); 29 | 30 | width: 100%; 31 | background-size: cover; 32 | background-repeat: no-repeat; 33 | background-position: 50%; 34 | border-radius: .5rem; 35 | position: relative; 36 | 37 | line-height: calc(15vw * 1.5); 38 | transition: box-shadow .3s ease; 39 | text-decoration: none; 40 | 41 | &:hover, &:focus { 42 | box-shadow: rgb(255 0 0 / 75%) 0px 3px 10px; 43 | outline-width: 0; 44 | 45 | } 46 | ` 47 | 48 | const Title = styled.label` 49 | font-size: 1.2rem; 50 | padding: .1rem; 51 | line-height: 1.6rem; 52 | overflow: hidden; 53 | color:white; 54 | ` 55 | 56 | const Bar = styled.div` 57 | position: absolute; 58 | display: flex; 59 | height: 4%; 60 | bottom: -5%; 61 | width: calc(100% - .7rem); 62 | background-color: black; 63 | border-radius: 10rem; 64 | 65 | & > div{ 66 | height:100%; 67 | border-radius: 10rem; 68 | background-color : var(--second-color); 69 | } 70 | ` 71 | 72 | const Name = styled.div` 73 | height: 1.5rem; 74 | color: white; 75 | width: calc(100%); 76 | text-overflow: ellipsis; 77 | overflow: hidden; 78 | white-space: nowrap; 79 | box-shadow: none !important; 80 | text-align: center; 81 | text-decoration: none; 82 | padding-top: 0.1rem; 83 | ` 84 | 85 | let timeout = null; 86 | 87 | const noCoverStyle = { 88 | display: "flex", 89 | alignItems: "center", 90 | backgroundColor: "#0c0f13",//randomColor[0], 91 | border: "0.2rem solid white", 92 | justifyContent: "center" 93 | } 94 | 95 | const RowItem = ({name, stream_icon, last, stream_id, category_id, id, style, isSeries, container_extension, existingTmdb}) => { 96 | const [noCover,setNoCover] = useState(!stream_icon); 97 | const [streamStat, setStreamStat] = useState() 98 | const [popup, setPopup] = useState(false); 99 | const [popupStyle, setPopupStyle] = useState({transform: "scale(0)"}) 100 | 101 | const styleLast={ 102 | position: "absolute", 103 | height: "calc(15vw * 1.5)", 104 | right: "calc(-7% - 0.4rem)", 105 | width: "100%", 106 | filter: "brightness(0.5)", 107 | pointerEvents: "none", 108 | cursor: "default", 109 | textDecoration: "none" 110 | } 111 | 112 | const showPopup = (mode) =>{ 113 | clearTimeout(timeout); 114 | if(mode === true){ 115 | timeout = setTimeout(()=>{ 116 | setPopup(true); 117 | setTimeout(()=>{ 118 | setPopupStyle({transform: "scale(1)"}) 119 | },100) 120 | },700); 121 | }else { 122 | setPopupStyle({transform: "scale(0)"}) 123 | setTimeout(()=>{ 124 | setPopup(false); 125 | },100) 126 | } 127 | } 128 | 129 | useEffect(() => { 130 | setStreamStat(DB.findOne(isSeries ? "series" : "movie",stream_id, category_id === "fav")) 131 | }, [isSeries, stream_id, category_id]) 132 | 133 | 134 | return ( 135 |
  • showPopup(true)} onMouseLeave={() => showPopup(false)} 139 | to={{ 140 | pathname:`/${isSeries ? "series" : "movie"}/category/${category_id}/${stream_id}/info/`, 141 | search: window.location.search, 142 | }} 143 | > 144 | 145 | {!noCover ? 146 | setNoCover(true)} alt={name}/> 147 | : 148 | {optimizeName(name)} 149 | } 150 | 151 | {!noCover && ({optimizeName(name)})} 152 | {streamStat && streamStat.tot > 3 && streamStat.tot < 95 && ( 153 | 154 |
    155 | 156 | )} 157 | {popup && !last && ()} 158 |
  • 159 | ) 160 | } 161 | 162 | export default RowItem -------------------------------------------------------------------------------- /src/components/Series/Seasons.js: -------------------------------------------------------------------------------- 1 | import React, {useState,useEffect} from 'react' 2 | import styled from "styled-components" 3 | import Episode from './Episode' 4 | import {useDispatch} from "react-redux" 5 | import {setPlaylistEpisodes} from "../../actions/set-Playlist-Episodes" 6 | import {useHistory, useLocation, useParams} from "react-router-dom"; 7 | import DB from "../../other/local-db" 8 | import {generateUrl} from "../../other/generate-url" 9 | 10 | const Select = styled.select` 11 | display: flex; 12 | align-items: center; 13 | min-width: 4em; 14 | position: relative; 15 | font-size: 1.3rem; 16 | background-color: rgb(36, 36, 36); 17 | color: white; 18 | cursor: pointer; 19 | border: 0.1em solid rgb(77, 77, 77); 20 | border-radius: 0.2em; 21 | outline: 0; 22 | font-weight: 600; 23 | padding: 0.3rem 0.5rem; 24 | 25 | transition: all 0.2s ease; 26 | 27 | &:focus, &:hover{ 28 | border-color: white; 29 | box-shadow: 0 0 0 0.2rem rgb(255 255 255 / 25%); 30 | } 31 | 32 | & > option{ 33 | 34 | } 35 | ` 36 | 37 | const Container = styled.div` 38 | 39 | margin-top:1rem; 40 | ` 41 | 42 | const Seasons = ({seasonData, cover}) => { 43 | const [seasons,setSeasons] = useState([]) 44 | const [streamStat, setStreamStat] = useState({}) 45 | const [selectedSeason, setSelectedSeason] = useState(); 46 | 47 | const {stream_id} = useParams(); 48 | 49 | const history = useHistory(); 50 | const location = useLocation(); 51 | const dispatch = useDispatch(); 52 | 53 | useEffect(()=>{ 54 | if(!seasonData) 55 | return; 56 | const s = Object.keys(seasonData).map(x => { 57 | return { 58 | id: parseInt(x), 59 | name: "Season " + x, 60 | episodes: seasonData[x].map(y => { 61 | return { 62 | ...y, 63 | episode_num: parseInt(y.episode_num), 64 | url: y.url || y.direct_source || generateUrl("series", y.id, y.container_extension), 65 | url2: generateUrl("series", y.id, y.container_extension) 66 | } 67 | }) 68 | } 69 | }); 70 | if(s.length > 0){ 71 | setSeasons(s); 72 | 73 | let stat = DB.findOne("series",stream_id); 74 | (!stat && (stat={id: stream_id, season: s[0].id, episode: s[0].episodes[0].episode_num, start:0})) 75 | 76 | stat.season = parseInt(stat.season); 77 | stat.episode = parseInt(stat.episode); 78 | 79 | if(stat.tot>95 || stat.tot< 3){ 80 | stat.tot = 0; 81 | let season = s.find(x=>x.id === stat.season); 82 | 83 | if(season.episodes[season.episodes.length-1].episode_num > stat.episode){ 84 | stat.episode++; 85 | }else{ 86 | season = s.find(x=>x.id === stat.season+1); 87 | if(season && season.episodes && season.episodes.length >0){ 88 | stat.season = season.id; 89 | stat.episode = season.episodes[0].episode_num 90 | }else stat={id: stream_id, season: s[0].id, episode: s[0].episodes[0].episode_num, start:0} 91 | } 92 | } 93 | 94 | setSelectedSeason(stat.season); 95 | setStreamStat(stat) 96 | } 97 | }, [seasonData, stream_id]) 98 | 99 | const playEpisode = (episode) =>{ 100 | const list = seasons.find(x=>x.id === selectedSeason).episodes.map(x=> {return{...x, id:x.info.id}}); 101 | dispatch(setPlaylistEpisodes(list)) 102 | history.push(location.pathname.replace("info",`play/season/${selectedSeason}/episode/${episode}`)) 103 | } 104 | 105 | return ( 106 | <> 107 |
    108 |
    109 |

    Episodes

    110 |
    111 |
    112 | 115 |
    116 |
    117 | 118 | {streamStat && seasons.find(x=> x.id === selectedSeason) && ( 119 | seasons.find(x=> x.id === selectedSeason).episodes.map(ep=> 120 | 132 | ) 133 | )} 134 | 135 | 136 | ) 137 | } 138 | 139 | export default Seasons 140 | -------------------------------------------------------------------------------- /src/components/Epg-Fullscreen/Programme.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useState, useEffect } from 'react' 4 | import {useSelector, useDispatch} from "react-redux" 5 | import {setEpgPopup} from "../../actions/epgPopup" 6 | 7 | const dateFormat = require("dateformat"); 8 | 9 | const Container = styled.li` 10 | position: absolute; 11 | left: 0; 12 | height: 100%; 13 | background-color: #fff; 14 | transition: all 0.3s ease 0s; 15 | 16 | &:hover, &:focus{ 17 | /*z-index: 2; 18 | width: fit-content !important; 19 | max-width: 600px; 20 | text-overflow: ellipsis; 21 | white-space: nowrap; 22 | overflow: hidden;*/ 23 | outline-width: 0px; 24 | } 25 | 26 | &:focus{ 27 | border: 1px solid #0071ff; 28 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 29 | outline-width: 0px; 30 | border-radius: .25rem; 31 | } 32 | 33 | & > div{ 34 | text-align: initial; 35 | position: relative; 36 | color: #303030; 37 | background-color: #f0f0f0; 38 | margin-right: 2px; 39 | margin-left: 2px; 40 | height: 100%; 41 | border-radius: .25rem; 42 | transition: all 0.3s ease 0s; 43 | 44 | 45 | &:hover, &:focus{ 46 | cursor: pointer; 47 | border: 1px solid #0071ff; 48 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 6%); 49 | outline-width: 0px; 50 | } 51 | } 52 | 53 | & > div > div{ 54 | display: flex; 55 | flex-direction: row; 56 | flex-wrap: nowrap; 57 | height: 100%; 58 | } 59 | ` 60 | const BodyData = styled.div` 61 | padding: 1rem 1.5rem; 62 | height: 100%; 63 | 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | overflow: hidden; 67 | ` 68 | 69 | const Title = styled.div` 70 | margin: 0; 71 | font-size: 1rem; 72 | font-weight: 500; 73 | line-height: 1.125rem; 74 | 75 | text-overflow: ellipsis; 76 | white-space: nowrap; 77 | overflow: hidden; 78 | ` 79 | 80 | const Description = styled.div` 81 | padding-top: .25rem; 82 | font-size: .875rem; 83 | max-width: 500px; 84 | flex-wrap: nowrap!important; 85 | 86 | text-overflow: ellipsis; 87 | white-space: nowrap; 88 | overflow: hidden; 89 | ` 90 | 91 | const Time = styled.div` 92 | padding-top: .75rem; 93 | font-size: .875rem; 94 | 95 | text-overflow: ellipsis; 96 | white-space: nowrap; 97 | overflow: hidden; 98 | ` 99 | const ProgressBar = styled.div` 100 | position: absolute; 101 | top: 0; 102 | bottom: 0; 103 | left: 0; 104 | right: 0; 105 | ` 106 | 107 | const Progress = styled.div` 108 | background: repeating-linear-gradient(45deg,rgba(0,0,0,.5),rgba(0,0,0,.5) 1px,#b4c6df 1px,#b4c6df 7px); 109 | opacity: .2; 110 | height: 100%; 111 | ` 112 | 113 | const isActive = { 114 | color: "#fff", 115 | backgroundColor: "#064497" 116 | } 117 | 118 | const Programme = ({start, stop, title, description, dayTime, chId, liveTime, catchup}) => { 119 | const dispatch = useDispatch(); 120 | 121 | const timer = useSelector(state => state.timer60); 122 | const h24Format = useSelector(state => state.h24); 123 | /* 124 | length = each minute is 0.25px; 125 | start = each minute is 2.73px 126 | */ 127 | 128 | /*const startDay = new Date(dayTime); 129 | startDay.setHours(0,0,0,0) 130 | const endDay = new Date(dayTime); 131 | endDay.setHours(23,59,59,9999) 132 | 133 | start < startDay.getTime() && (start = startDay.getTime()) 134 | stop > endDay.getTime() && (stop = endDay.getTime())*/ 135 | 136 | const [activeLength, setActiveLenght] = useState(liveTime===true && stop > timer && start <= timer ? ((timer-start)/(stop-start))*100 : false); 137 | const lengthStyle = ((stop-start)/3600000)*17; 138 | const startStyle = -2+((start-dayTime)/3600000)*17; 139 | const Spacing = { 140 | minWidth : lengthStyle + "rem", 141 | width : lengthStyle + "rem", 142 | transform: `translateX(${startStyle}rem)` 143 | } 144 | 145 | 146 | useEffect(() => { 147 | if(liveTime === false) 148 | return; 149 | if(!(stop > timer && start <= timer)){ 150 | if(activeLength) 151 | setActiveLenght(false); 152 | }else setActiveLenght(((timer-start)/(stop-start))*100) 153 | }, [timer,start,stop,activeLength, liveTime]); 154 | 155 | 156 | return ( 157 | dispatch(setEpgPopup( 158 | { 159 | title: title, 160 | description: description, 161 | start:start, stop:stop, 162 | catchup: catchup, 163 | chId} 164 | ))} className={activeLength > 0 ? "isActive" : "noActive"} id={`ch${chId}-${start}`}> 165 |
    0 ? isActive : {}}> 166 |
    167 | 168 | {catchup > 0 ? (<i className="fas fa-history pr-2"></i>) : ""}{title} 169 | {description} 170 | 171 | 172 | 173 | 174 | 175 |
    176 |
    177 |
    178 | ) 179 | } 180 | 181 | export default Programme 182 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.js: -------------------------------------------------------------------------------- 1 | import React, {useState,useEffect} from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Container = styled.div` 5 | position: fixed; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate(-50%, -50%); 9 | ` 10 | 11 | const Message = styled.div` 12 | padding: 0rem 2rem; 13 | color: #fff; 14 | box-shadow: 0 15px 35px rgb(50 50 93 / 20%), 0 5px 15px rgb(0 0 0 / 17%); 15 | pointer-events: auto; 16 | border: 0 solid rgba(0, 0, 0, .2); 17 | border-radius: .4375rem; 18 | outline: 0; 19 | ` 20 | 21 | const Header = styled.div` 22 | border-color: rgba(255, 255, 255, .075); 23 | display: flex; 24 | padding: 1.25rem; 25 | border-bottom: 0 solid #e9ecef; 26 | border-top-left-radius: .4375rem; 27 | border-top-right-radius: .4375rem; 28 | align-items: flex-start; 29 | justify-content: space-between; 30 | 31 | & > h6{ 32 | font-size: 1.0625rem; 33 | line-height: 1.1; 34 | margin-bottom: 0; 35 | } 36 | 37 | & > button{ 38 | overflow: visible; 39 | text-transform: none; 40 | font-size: 1.5rem; 41 | font-weight: 600; 42 | line-height: 1; 43 | float: right; 44 | text-shadow: none; 45 | transition: all .15s ease; 46 | border: 0; 47 | appearance: none; 48 | cursor: pointer; 49 | text-decoration: none; 50 | margin: -1rem -1rem -1rem auto; 51 | padding: 1.25rem; 52 | color: rgba(0, 0, 0, .9); 53 | outline: 0; 54 | background-color: transparent; 55 | opacity: .75; 56 | 57 | & > span{ 58 | color:white; 59 | } 60 | 61 | } 62 | ` 63 | 64 | const Body = styled.div` 65 | position: relative; 66 | padding: 1.5rem; 67 | flex: 1 1 auto; 68 | text-align: center; 69 | 70 | & > h4{ 71 | font-size: .95rem; 72 | font-weight: 600; 73 | letter-spacing: .025em; 74 | text-transform: uppercase; 75 | } 76 | 77 | 78 | & > i{ 79 | font-size: 3em; 80 | line-height:5rem; 81 | } 82 | ` 83 | 84 | const Footer = styled.div` 85 | display: flex; 86 | padding: 1.25rem; 87 | border-top: 0 solid #e9ecef; 88 | border-bottom-right-radius: .4375rem; 89 | border-bottom-left-radius: .4375rem; 90 | flex-wrap: wrap; 91 | align-items: center; 92 | justify-content: flex-end; 93 | border-color: rgba(255, 255, 255, .075); 94 | ` 95 | 96 | const ErrorButton = styled.button` 97 | color: #212529; 98 | border-color: #fff; 99 | background-color: #e6e6e6; 100 | box-shadow: none; 101 | text-decoration: none; 102 | cursor: pointer; 103 | 104 | &:hover{ 105 | background-color: #829196; 106 | color: white; 107 | border-color: #829196; 108 | box-shadow: 0 0 0 0.2rem #82919608; 109 | -webkit-transition: all .5s ease; 110 | transition: all .5s ease; 111 | background-color: var(--second-color); 112 | } 113 | ` 114 | 115 | const SafeButton = styled.button` 116 | color: #212529; 117 | border-color: #fff; 118 | background-color: #e6e6e6; 119 | box-shadow: none; 120 | text-decoration: none; 121 | cursor: pointer; 122 | 123 | &:hover{ 124 | background-color: #5536f5; 125 | color: white; 126 | border-color: #829196; 127 | box-shadow: 0 0 0 0.2rem #82919608; 128 | -webkit-transition: all .5s ease; 129 | transition: all .5s ease; 130 | } 131 | ` 132 | 133 | 134 | const Popup = ({type,title,description,icon, onclick, error=true, unsecure}) => { 135 | const [style, setStyle] = useState({ 136 | opacity: 0, 137 | transform: "scale(0.9)", 138 | background: error === true ? "linear-gradient(87deg, #f5365c 0, #f56036 100%)" : "linear-gradient(87deg, #6536f5 0px, #36bbf5 100%)" 139 | }); 140 | 141 | useEffect(() => { 142 | setStyle({ 143 | opacity: 1, 144 | transform: "translateX(0)", 145 | transition: "opacity 300ms, transform 300ms", 146 | background: error === true ? "linear-gradient(87deg, #f5365c 0, #f56036 100%)" : "linear-gradient(87deg, #6536f5 0px, #36bbf5 100%)" 147 | }) 148 | },[]); 149 | 150 | const onCloseEvent = () =>{ 151 | setStyle({ 152 | opacity: 0, 153 | transform: "scale(0.9)", 154 | transition: "opacity 300ms, transform 300ms", 155 | background: error === true ? "linear-gradient(87deg, #f5365c 0, #f56036 100%)" : "linear-gradient(87deg, #6536f5 0px, #36bbf5 100%)" 156 | }) 157 | 158 | setTimeout(()=>{ 159 | onclick && (onclick()); 160 | },150) 161 | } 162 | 163 | return ( 164 | 165 | 166 |
    167 |
    168 | 169 |
    170 | 171 | 172 |

    {title}

    173 | {unsecure === true ? 174 | (

    ) 175 | : 176 | (

    {description}

    ) 177 | } 178 | 179 |
    180 | {error === true ? 181 | (Ok, got it) 182 | : 183 | (Ok, got it) 184 | } 185 |
    186 |
    187 |
    188 | ) 189 | } 190 | 191 | export default Popup 192 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/components/AccountInfo.js: -------------------------------------------------------------------------------- 1 | import React, {useState,useEffect} from 'react' 2 | import "./MainMenu/MainMenu" 3 | import styled from "styled-components" 4 | import {getInfo, logout} from "../other/user_info" 5 | import {useHistory} from 'react-router-dom' 6 | import {useSelector, useDispatch} from "react-redux" 7 | import {setH24} from "../actions/h24" 8 | 9 | const Container = styled.div` 10 | background-color: #212529; 11 | position:absolute; 12 | width:100%; 13 | height:100%; 14 | top:0; 15 | left:0; 16 | ` 17 | 18 | const Box = styled.div` 19 | position: fixed; 20 | z-index: 98; 21 | &:before, &:after { 22 | content: ""; 23 | position: fixed; 24 | width: 100vw; 25 | height: 100vh; 26 | background: rgba(20, 21, 26,0.6); 27 | border-bottom-left-radius: 200%; 28 | z-index: -1; 29 | -webkit-transition: -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 30 | transition: -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 31 | transition: transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 32 | transition: transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, -webkit-transform cubic-bezier(0.77, 0, 0.175, 1) 0.6s, border-radius linear 0.8s; 33 | -webkit-transform: translateX(100%) translateY(-100%); 34 | transform: translateX(100%) translateY(-100%); 35 | } 36 | &:after { 37 | background: rgba(9,9,12,1); 38 | -webkit-transition-delay: 0s; 39 | transition-delay: 0s; 40 | } 41 | &:before { 42 | -webkit-transition-delay: .2s; 43 | transition-delay: .2s; 44 | } 45 | ` 46 | 47 | const Li = styled.li` 48 | font-size: 3rem; 49 | text-align: left; 50 | 51 | & > button{ 52 | text-transform: none !important; 53 | } 54 | 55 | & > button:after{ 56 | height: 0px !important; 57 | } 58 | ` 59 | 60 | const Button = styled.button` 61 | color: #fff; 62 | background-color: var(--first-color) !important; 63 | transition: all .5s ease; 64 | width:100%; 65 | 66 | &:after{ 67 | height: 0px !important; 68 | } 69 | 70 | &:hover, &:focus{ 71 | background-color: #829196; 72 | color:white; 73 | border-color: #829196; 74 | box-shadow: 0 0 0 0.2rem #82919608; 75 | transition: all .5s ease; 76 | } 77 | ` 78 | 79 | const ButtonLogout = styled.button` 80 | color: #fff; 81 | background-color: var(--second-color) !important; 82 | transition: all .5s ease; 83 | width:100%; 84 | 85 | &:after{ 86 | height: 0px !important; 87 | } 88 | 89 | &:hover, &:focus{ 90 | background-color: var(--second-color) !important; 91 | color:white; 92 | border-color: #829196; 93 | box-shadow: 0 0 0 0.2rem #82919608; 94 | transition: all .5s ease; 95 | } 96 | ` 97 | const Checkbox = styled.input` 98 | font-size: 3rem; 99 | height: 3rem; 100 | width: 3rem; 101 | ` 102 | 103 | 104 | 105 | const AccountInfo = () => { 106 | const [style, setStyle] = useState(); 107 | const info = getInfo(); 108 | const history = useHistory(); 109 | const dispatch = useDispatch() 110 | const h24 = useSelector(state => state.h24) 111 | 112 | useEffect(() => { 113 | setTimeout(()=>{ 114 | setStyle("nav-active"); 115 | document.getElementById("info-back").focus(); 116 | },10) 117 | },[]); 118 | 119 | 120 | const back = (page) =>{ 121 | setStyle(); 122 | setTimeout(()=>{ 123 | history.goBack(); 124 | },600) 125 | } 126 | 127 | return ( 128 | 129 | 130 |
    131 |
      132 |
    • Username:
    • 133 |
    • Password:
    • 134 |
    • Max connections:
    • 135 |
    • Expire on:
    • 136 |
    • {info.message}
    • 137 |
    • 138 |
      139 | dispatch(setH24(h24!=="HH:MM"))} id="h24"/> 140 | 143 |
      144 |
    • 145 |
      146 |
      147 |
    • back()}>
    • 148 |
      149 |
      150 |
    • LOGOUT
    • 151 |
      152 |
      153 | 154 |
    155 |
    156 |
    157 |
    158 | ) 159 | } 160 | 161 | export default AccountInfo 162 | -------------------------------------------------------------------------------- /src/components/MainMenu/MainMenu.css: -------------------------------------------------------------------------------- 1 | div.nav-active .nav__content { 2 | visibility: visible; 3 | } 4 | div.nav-active .menu-icon__line { 5 | background-color: #fff; 6 | -webkit-transform: translate(0px, 0px) rotate(-45deg); 7 | transform: translate(0px, 0px) rotate(-45deg); 8 | } 9 | div.nav-active .menu-icon__line-left { 10 | width: 15px; 11 | -webkit-transform: translate(2px, 4px) rotate(45deg); 12 | transform: translate(2px, 4px) rotate(45deg); 13 | } 14 | div.nav-active .menu-icon__line-right { 15 | width: 15px; 16 | float: right; 17 | -webkit-transform: translate(-3px, -3.5px) rotate(45deg); 18 | transform: translate(-3px, -3.5px) rotate(45deg); 19 | } 20 | div.nav-active .menu-icon:hover .menu-icon__line-left, 21 | div.nav-active .menu-icon:hover .menu-icon__line-right, 22 | div.nav-active .menu-icon:focus .menu-icon__line-left, 23 | div.nav-active .menu-icon:focus .menu-icon__line-right { 24 | width: 15px; 25 | } 26 | div.nav-active .nav { 27 | visibility: visible; 28 | } 29 | div.nav-active .nav:before, div.nav-active .nav:after { 30 | -webkit-transform: translateX(0%) translateY(0%); 31 | transform: translateX(0%) translateY(0%); 32 | border-radius: 0; 33 | } 34 | div.nav-active .nav:after { 35 | -webkit-transition-delay: .1s; 36 | transition-delay: .1s; 37 | } 38 | div.nav-active .nav:before { 39 | -webkit-transition-delay: 0s; 40 | transition-delay: 0s; 41 | } 42 | div.nav-active .nav__list-item { 43 | opacity: 1; 44 | -webkit-transform: translateX(0%); 45 | transform: translateX(0%); 46 | -webkit-transition: opacity .3s ease, color .3s ease, -webkit-transform .3s ease; 47 | transition: opacity .3s ease, color .3s ease, -webkit-transform .3s ease; 48 | transition: opacity .3s ease, transform .3s ease, color .3s ease; 49 | transition: opacity .3s ease, transform .3s ease, color .3s ease, -webkit-transform .3s ease; 50 | } 51 | div.nav-active .nav__list-item:nth-child(0) { 52 | -webkit-transition-delay: 0.7s; 53 | transition-delay: 0.7s; 54 | } 55 | div.nav-active .nav__list-item:nth-child(1) { 56 | -webkit-transition-delay: 0.8s; 57 | transition-delay: 0.8s; 58 | } 59 | div.nav-active .nav__list-item:nth-child(2) { 60 | -webkit-transition-delay: 0.9s; 61 | transition-delay: 0.9s; 62 | } 63 | div.nav-active .nav__list-item:nth-child(3) { 64 | -webkit-transition-delay: 1s; 65 | transition-delay: 1s; 66 | } 67 | div.nav-active .nav__list-item:nth-child(4) { 68 | -webkit-transition-delay: 1.1s; 69 | transition-delay: 1.1s; 70 | } 71 | div.nav-active .nav__list-item:nth-child(5) { 72 | -webkit-transition-delay: 1.2s; 73 | transition-delay: 1.2s; 74 | } 75 | div.nav-active .nav__list-item:nth-child(6) { 76 | -webkit-transition-delay: 1.3s; 77 | transition-delay: 1.3s; 78 | } 79 | div.nav-active .nav__list-item:nth-child(7) { 80 | -webkit-transition-delay: 1.4s; 81 | transition-delay: 1.4s; 82 | } 83 | div.nav-active .nav__list-item:nth-child(8) { 84 | -webkit-transition-delay: 1.5s; 85 | transition-delay: 1.5s; 86 | } 87 | div.nav-active .nav__list-item:nth-child(9) { 88 | -webkit-transition-delay: 1.6s; 89 | transition-delay: 1.6s; 90 | } 91 | div.nav-active .nav__list-item:nth-child(10) { 92 | -webkit-transition-delay: 1.7s; 93 | transition-delay: 1.7s; 94 | } 95 | 96 | 97 | 98 | 99 | 100 | .nav__content { 101 | position: fixed; 102 | visibility: hidden; 103 | top: 50%; 104 | margin-top: 20px; 105 | -webkit-transform: translate(0%, -50%); 106 | transform: translate(0%, -50%); 107 | width: 100%; 108 | text-align: center; 109 | } 110 | .nav__list { 111 | position: relative; 112 | padding: 0; 113 | margin: 0; 114 | z-index: 2; 115 | } 116 | .nav__list-item { 117 | position: relative; 118 | display: block; 119 | -webkit-transition-delay: 0.8s; 120 | transition-delay: 0.8s; 121 | opacity: 0; 122 | text-align: center; 123 | color: #fff; 124 | overflow: hidden; 125 | font-size: 5rem; 126 | font-weight: 900; 127 | line-height: 1.15; 128 | letter-spacing: 3px; 129 | -webkit-transform: translate(100px, 0%); 130 | transform: translate(100px, 0%); 131 | -webkit-transition: opacity .2s ease, -webkit-transform .3s ease; 132 | transition: opacity .2s ease, -webkit-transform .3s ease; 133 | transition: opacity .2s ease, transform .3s ease; 134 | transition: opacity .2s ease, transform .3s ease, -webkit-transform .3s ease; 135 | margin-top: 0; 136 | margin-bottom: 0; 137 | } 138 | .nav__list-item button{ 139 | background-color: transparent; 140 | border: none; 141 | outline: none; 142 | 143 | position: relative; 144 | text-decoration: none; 145 | color: rgba(255,255,255,0.6); 146 | overflow: hidden; 147 | cursor: pointer; 148 | padding-left: 5px; 149 | padding-right: 5px; 150 | font-weight: 900; 151 | z-index: 2; 152 | display: inline-block; 153 | text-transform: uppercase; 154 | -webkit-transition: all 200ms linear; 155 | transition: all 200ms linear; 156 | } 157 | .nav__list-item button:after{ 158 | position: absolute; 159 | content: ''; 160 | top: 50%; 161 | margin-top: -2px; 162 | left: 50%; 163 | width: 0; 164 | height: 0; 165 | opacity: 0; 166 | background-color: var(--second-color); 167 | z-index: 1; 168 | -webkit-transition: all 200ms linear; 169 | transition: all 200ms linear; 170 | } 171 | 172 | .nav__list-item{ 173 | outline-width: 0; 174 | } 175 | 176 | .nav__list-item button:hover:after, 177 | .nav__list-item:focus button:after{ 178 | height: 4px; 179 | opacity: 1; 180 | left: 0; 181 | width: 100%; 182 | } 183 | .nav__list-item button:hover, 184 | .nav__list-item:focus button{ 185 | color: rgba(255,255,255,1); 186 | } 187 | .nav__list-item.active-nav button{ 188 | color: rgba(255,255,255,1); 189 | } 190 | .nav__list-item.active-nav button:after{ 191 | height: 4px; 192 | opacity: 1; 193 | left: 0; 194 | width: 100%; 195 | } -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, {useState,useRef,useEffect} from 'react' 2 | import styled from 'styled-components'; 3 | import {useHistory} from "react-router-dom"; 4 | import {useAuth} from "../other/auth" 5 | import Popup from "./Popup/Popup" 6 | import Cookies from 'js-cookie' 7 | 8 | const Container = styled.div` 9 | position:absolute; 10 | width:100%; 11 | height:100%; 12 | top:0; 13 | left:0; 14 | background-color: var(--first-color); 15 | transition: all .5s ease; 16 | overflow:hidden; 17 | 18 | & > * >input{ 19 | background-color: var(--first-color); 20 | color:white; 21 | } 22 | ` 23 | 24 | const Box = styled.form` 25 | width: 50vw; 26 | left: 25vw; 27 | top: 25vh; 28 | position: absolute; 29 | 30 | & > h2, & > h5{ 31 | color:white; 32 | text-align: center; 33 | } 34 | 35 | & > label{ 36 | color:white; 37 | } 38 | ` 39 | 40 | const Input = styled.input` 41 | margin-bottom: 1rem; 42 | 43 | &:focus{ 44 | color:white; 45 | border-color: var(--second-color); 46 | box-shadow: 0 0 0 0.2rem var(--second-color-shadow); 47 | background-color: var(--first-color); 48 | transition: all .5s ease; 49 | } 50 | ` 51 | 52 | const Button = styled.button` 53 | color: #fff; 54 | background-color: var(--second-color); 55 | transition: all .5s ease; 56 | width:100%; 57 | 58 | &:hover, &:focus{ 59 | background-color: #829196; 60 | color:white; 61 | border-color: #829196; 62 | box-shadow: 0 0 0 0.2rem #82919608; 63 | transition: all .5s ease; 64 | } 65 | ` 66 | 67 | const Login = ({url}) => { 68 | 69 | const [dns, setDns] = useState(""); 70 | const [username, setUsername] = useState(""); 71 | const [password, setPassword] = useState(""); 72 | const [showPopup, setShowPopup] = useState(false); 73 | const [blur, setBlur] = useState(); 74 | const [m3u8, setM3u8] = useState(window.m3u8warning === true && !Cookies.get("m3u8_play")); 75 | 76 | const history = useHistory(); 77 | const auth = useAuth(); 78 | 79 | useEffect(() => { 80 | if(auth.isAuth()) 81 | history.push(url||"/") 82 | else auth.authLogin(()=>history.push(url||"/")) 83 | },[auth,history]); 84 | 85 | useEffect(()=>{ 86 | m3u8 ? setBlur({filter:"blur(.3rem)"}) : setBlur({}); 87 | },[m3u8]) 88 | 89 | const inputRef = useRef(0) 90 | 91 | const remoteController = (event) => { 92 | let active = document.activeElement; 93 | if (event.keyCode === 40 && active.nextSibling) { 94 | if(active.nextSibling.tagName==="LABEL") 95 | active = active.nextSibling; 96 | active.nextSibling.focus(); 97 | } else if (event.keyCode === 38 && active.previousSibling) { 98 | if(active.previousSibling.tagName==="LABEL") 99 | active = active.previousSibling; 100 | active.previousSibling.focus(); 101 | } else if (event.keyCode === 13) 102 | active.click(); 103 | } 104 | 105 | const login = (e) =>{ 106 | e.preventDefault(); 107 | setBlur({filter:"blur(.3rem)"}) 108 | auth.signin(dns ,username,password, 109 | () => window.location="/"//history.replace("/") 110 | , 111 | (title,description) => { 112 | setShowPopup({title:title,description:description}); 113 | } 114 | ) 115 | } 116 | 117 | const closePopup = () =>{ 118 | setBlur({filter:"blur(0)"}); 119 | setShowPopup(false); 120 | inputRef.current.focus(); 121 | } 122 | 123 | return ( 124 | <> 125 | 126 | 127 | {false && (

    Welcome to {window.playername}

    )} 128 |

    129 | 130 |
    Write your credentials to continue
    131 |
    132 | {!window.dns && 133 | ([ 134 | , 135 | setDns(e.target.value)} value={dns} /> 136 | ]) 137 | } 138 | 139 | setUsername(e.target.value)} value={username} /> 140 | 141 | setPassword(e.target.value)} value={password}/> 142 | 143 |
    144 |
    145 | {showPopup && } 146 | {m3u8 && ( 147 | IPTVEditor Web Player can play live channels streams only in M3U8 format.
    The conversion will be done automatically if streams are in Xtreamcodes format (this won't affect your playlist)."} icon={"fas fa-info-circle"} onclick={()=> {Cookies.set("m3u8_play",1,{ expires: 365 }); setM3u8(!m3u8);}}/> 148 | )} 149 | {/*showKeyboard !== false && isMag && ()*/} 150 | 151 | ) 152 | } 153 | 154 | export default Login 155 | --------------------------------------------------------------------------------