From b6f87a29c41739fdd8b46490a6a90a02ed3e0a85 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Mon, 13 Apr 2026 16:15:48 -0300 Subject: [PATCH 1/6] style(Discover): update EPG day styles to match the calendar --- src/routes/Discover/EpgGuide/EpgGuide.less | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/routes/Discover/EpgGuide/EpgGuide.less b/src/routes/Discover/EpgGuide/EpgGuide.less index 0c9805e48..3e0858295 100644 --- a/src/routes/Discover/EpgGuide/EpgGuide.less +++ b/src/routes/Discover/EpgGuide/EpgGuide.less @@ -88,16 +88,20 @@ } .epg-day-weekday { - font-size: 1.1rem; - opacity: 0.65; - text-transform: uppercase; - letter-spacing: 0.04em; + font-size: 1rem; + font-weight: 500; + line-height: 100%; + color: var(--primary-foreground-color); + opacity: 0.5; } .epg-day-date { - font-size: 1.6rem; - font-weight: 600; - line-height: 1.2; + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .epg-header-row { From db16d4fdeab43cef52b88b283f71d33d83b121f9 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Mon, 13 Apr 2026 20:17:47 -0300 Subject: [PATCH 2/6] style(Discover): refine EPG row styles for improved layout and visual consistency --- .../EpgGuide/EpgGuideRow/EpgGuideRow.less | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/src/routes/Discover/EpgGuide/EpgGuideRow/EpgGuideRow.less b/src/routes/Discover/EpgGuide/EpgGuideRow/EpgGuideRow.less index 943f202b9..5acadf269 100644 --- a/src/routes/Discover/EpgGuide/EpgGuideRow/EpgGuideRow.less +++ b/src/routes/Discover/EpgGuide/EpgGuideRow/EpgGuideRow.less @@ -4,12 +4,9 @@ @import (reference) '~stremio/common/screen-sizes.less'; .epg-row { - position: relative; display: flex; - height: 68px; + height: 60px; flex-shrink: 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - box-sizing: border-box; } .epg-program-list { @@ -21,54 +18,60 @@ .epg-program-block { position: absolute; - top: 4px; - bottom: 4px; - padding-right: 3px; - box-sizing: border-box; + padding-right: 4px; + width: 100%; &:hover { z-index: 10; + min-width: max-content; } &-inner { - display: flex; - align-items: center; - gap: 0.6rem; width: 100%; - height: 100%; - padding: 0 0.8rem 0 0.6rem; - border-radius: 0.6rem; - background-color: rgba(255, 255, 255, 0.07); - cursor: pointer; - overflow: hidden; + height: 56px; + display: flex; box-sizing: border-box; + border-radius: 0.5rem; + padding: 0 1rem 0 0.5rem; + cursor: pointer; + text-align: left; + align-items: center; + gap: 0.5rem; + color: var(--color-foreground); + background-color: #1E1C2E; transition: background-color 0.12s; &:hover { - background-color: rgba(255, 255, 255, 0.12); + background-color: #2B293F; + z-index: 10; + min-width: max-content; + min-height: 56px; + height: auto; + overflow: visible; } } &-current { .epg-program-block-inner { - background-color: rgba(94, 79, 162, 0.35); - border-left: 2.5px solid rgb(94, 79, 162); + background-color: #5e4fa2; &:hover { - background-color: rgba(94, 79, 162, 0.5); + background-color: var(--primary-accent-color); } } } } .epg-program-thumb { - flex: none; - width: 3rem; - height: 3rem; - border-radius: 0.4rem; + width: 2.5rem; + min-width: 2.5rem; + height: 2.5rem; + border-radius: 0.375rem; background-size: cover; background-position: center; - background-color: var(--overlay-color); + background-color: var(--color-surface); + flex-shrink: 0; + overflow: hidden; } .epg-program-content { @@ -77,12 +80,12 @@ display: flex; flex-direction: column; justify-content: center; - gap: 0.15rem; + gap: 0.125rem; } .epg-program-title { - font-size: 1.2rem; - font-weight: 500; + font-size: 13px; + font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -90,12 +93,12 @@ } .epg-program-time { - font-size: 1.05rem; + font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--primary-foreground-color); - opacity: 0.45; + opacity: 0.3; } .epg-now-line { From 8301f552c85ae4b4128128c36f738e047adb4ee9 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Mon, 13 Apr 2026 20:17:56 -0300 Subject: [PATCH 3/6] style(Discover): enhance EPG header and channel styles for better layout and visual clarity --- src/routes/Discover/EpgGuide/EpgGuide.less | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/routes/Discover/EpgGuide/EpgGuide.less b/src/routes/Discover/EpgGuide/EpgGuide.less index 3e0858295..9587e2fac 100644 --- a/src/routes/Discover/EpgGuide/EpgGuide.less +++ b/src/routes/Discover/EpgGuide/EpgGuide.less @@ -105,11 +105,10 @@ } .epg-header-row { - flex: none; display: flex; - flex-direction: row; - border-bottom: 1px solid var(--overlay-color); + flex-shrink: 0; margin-left: 1.5rem; + border-bottom: 2px solid var(--color-surface); } .epg-channel-column-header { @@ -117,9 +116,8 @@ display: flex; align-items: center; justify-content: center; - padding-bottom: 0.4rem; - border-right: 1px solid rgba(255, 255, 255, 0.07); - background-color: rgba(0, 0, 0, 0.25); + padding: 0 0.5rem; + margin: 0 4px 4px 0; position: relative; z-index: 20; } @@ -233,8 +231,8 @@ .epg-channel-column { flex-shrink: 0; overflow: hidden; - background-color: rgba(0, 0, 0, 0.25); - border-right: 1px solid rgba(255, 255, 255, 0.07); + // background-color: rgba(0, 0, 0, 0.25); + // border-right: 1px solid rgba(255, 255, 255, 0.07); } .epg-channel-column-inner { @@ -242,12 +240,13 @@ } .epg-channel-cell { + background: var(--overlay-color); display: flex; align-items: center; justify-content: center; padding: 0.5rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - box-sizing: border-box; + border-radius: 0.5rem; + margin: 0 4px 4px 0; } .epg-channel-logo { From ca7a22f0130a7578462611dd121f2c0476790bb2 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Mon, 13 Apr 2026 20:18:10 -0300 Subject: [PATCH 4/6] style(Discover): adjust EPG row height and maximum pixel scale for improved layout --- src/routes/Discover/EpgGuide/EpgGuide.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Discover/EpgGuide/EpgGuide.tsx b/src/routes/Discover/EpgGuide/EpgGuide.tsx index cbb927c53..86cbf4943 100644 --- a/src/routes/Discover/EpgGuide/EpgGuide.tsx +++ b/src/routes/Discover/EpgGuide/EpgGuide.tsx @@ -7,10 +7,10 @@ import { programStartMs, programEndMs } from './epgUtils'; import styles from './EpgGuide.less'; const BASE_PIXELS_PER_HOUR = 120; // minimum scale -const MAX_PIXELS_PER_HOUR = 720; // cap — at 720 a 10-min show is 120 px (~17 000 px total grid) +const MAX_PIXELS_PER_HOUR = 360; // cap — at 720 a 10-min show is 120 px (~17 000 px total grid) const MIN_PROGRAM_WIDTH = 120; // px — wide enough to show a thumbnail + label const CHANNEL_COLUMN_WIDTH = 130; -const ROW_HEIGHT = 68; +const ROW_HEIGHT = 56; const HOUR_IN_MS = 60 * 60 * 1000; const DAY_IN_MS = 24 * HOUR_IN_MS; const MIN_PROGRAM_DURATION_MS = 10 * 60 * 1000; // ignore sub-10-min filler when choosing scale From 793a70646cee7a6cf048fcf94d35e84dc0fe26d5 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Thu, 16 Apr 2026 21:20:29 -0300 Subject: [PATCH 5/6] fix(Discover): update date handling in useEPG hook for improved type safety --- src/routes/Discover/EpgGuide/useEPG.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/Discover/EpgGuide/useEPG.ts b/src/routes/Discover/EpgGuide/useEPG.ts index 82397d34b..3c1a7b414 100644 --- a/src/routes/Discover/EpgGuide/useEPG.ts +++ b/src/routes/Discover/EpgGuide/useEPG.ts @@ -70,12 +70,11 @@ export const useEPG = (addon: Addon | null, date?: string | Date): EPGData => { }); const dateStr = useMemo(() => { - const d = date ? new Date(date) : new Date(); - + if (typeof date === 'string') return date; + const d = date instanceof Date ? date : new Date(); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; }, [date]); From 562e71c8511f73e6be9b2919064c5a8992bf0f66 Mon Sep 17 00:00:00 2001 From: mrcanelas Date: Thu, 16 Apr 2026 21:21:13 -0300 Subject: [PATCH 6/6] feat(Discover): add EpgProgramModal for detailed program view and update Discover component to manage program selection --- src/routes/Discover/Discover.js | 35 +-- .../EpgProgramModal/EpgProgramModal.less | 229 ++++++++++++++++++ .../EpgProgramModal/EpgProgramModal.tsx | 161 ++++++++++++ 3 files changed, 408 insertions(+), 17 deletions(-) create mode 100644 src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.less create mode 100644 src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.tsx diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index b1e83c385..542b0fcf2 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -11,7 +11,8 @@ const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); const useInstalledAddons = require('../Addons/useInstalledAddons'); -const {default: EpgGuide} = require('./EpgGuide'); +const { default: EpgGuide } = require('./EpgGuide'); +const { default: EpgProgramModal } = require('./EpgGuide/EpgProgramModal/EpgProgramModal'); const styles = require('./styles'); const SCROLL_TO_BOTTOM_THRESHOLD = 400; @@ -28,7 +29,7 @@ const Discover = ({ urlParams, queryParams }) => { const metasContainerRef = React.useRef(); const metaPreviewRef = React.useRef(); - const [selectedProgram, setSelectedProgram] = React.useState(null); + const [epgProgramModal, setEpgProgramModal] = React.useState(null); const installedAddons = useInstalledAddons({ transportUrl: null, catalogId: null }); const selectedAddon = React.useMemo(() => { const selected = discover.selectable.catalogs.find(({ selected }) => selected)?.addon ?? null; @@ -36,9 +37,9 @@ const Discover = ({ urlParams, queryParams }) => { return addon; }, [discover.selectable.catalogs, installedAddons]); const isEpgLayout = React.useMemo(() => !!selectedAddon?.manifest?.behaviorHints?.epgEndpoint, [selectedAddon]); - const onProgramSelect = React.useCallback((program) => { - setSelectedProgram(program); - }, [setSelectedProgram]); + const onProgramSelect = React.useCallback((program, channel) => { + setEpgProgramModal({ program, channel }); + }, []); React.useEffect(() => { if (!isEpgLayout && discover.catalog?.content.type === 'Loading' && metasContainerRef.current) { @@ -110,7 +111,7 @@ const Discover = ({ urlParams, queryParams }) => { closeInputsModal(); closeAddonModal(); setSelectedMetaItemIndex(0); - setSelectedProgram(null); + setEpgProgramModal(null); }, [discover.selected]); const renderEmptyState = () => ( @@ -180,17 +181,7 @@ const Discover = ({ urlParams, queryParams }) => { const renderMetaPreview = () => { if (isEpgLayout) { - if (selectedProgram === null) return null; - return ( - - ); + return null; } if (selectedMetaItem !== null) { @@ -260,6 +251,16 @@ const Discover = ({ urlParams, queryParams }) => { {renderMetaPreview()} + { + isEpgLayout && epgProgramModal !== null ? + setEpgProgramModal(null)} + /> + : + null + } { inputsModalOpen ? diff --git a/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.less b/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.less new file mode 100644 index 000000000..fea63d4b5 --- /dev/null +++ b/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.less @@ -0,0 +1,229 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.addon-details-modal-container { + +} + +:global(.modal-container.epg-program-modal) { + .epg-program-modal-body { + width: 40rem; + max-width: 100%; + color: var(--primary-foreground-color); + } + + .epg-program-logo-wrap { + display: flex; + justify-content: center; + align-items: center; + min-height: 5rem; + } + + .epg-program-logo { + display: block; + max-width: 100%; + max-height: 6rem; + object-fit: contain; + object-position: center; + } + + .epg-program-logo-placeholder { + font-size: 1.35rem; + font-weight: 600; + text-align: center; + color: var(--primary-foreground-color); + opacity: 0.85; + } + + .epg-program-title { + margin: -0.25rem 0 0; + font-size: 1.05rem; + font-weight: 600; + text-align: center; + line-height: 1.35; + color: var(--primary-foreground-color); + } + + .epg-program-meta-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.75rem 1.5rem; + font-size: 1.1rem; + font-weight: 600; + color: var(--primary-foreground-color); + } + + .epg-program-description { + margin: 0; + font-size: 0.95rem; + font-weight: 400; + line-height: 1.65; + color: fade(@color-surface-light5, 88%); + text-align: left; + } + + .epg-program-streams { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 14rem; + overflow-y: auto; + margin: 0.25rem 0; + } + + .epg-program-stream-empty { + padding: 1rem 0.5rem; + font-size: 0.95rem; + text-align: center; + color: fade(@color-surface-light5, 65%); + } + + .epg-program-stream-row { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.65rem 0.65rem 0.65rem 0.85rem; + border: none; + border-radius: var(--border-radius); + background: fade(@color-surface-light5, 6%); + color: inherit; + text-align: left; + cursor: pointer; + transition: background 0.15s ease; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0.35rem; + bottom: 0.35rem; + width: 3px; + border-radius: 2px; + background: transparent; + transition: background 0.15s ease; + } + + &:hover, + &:focus-visible { + background: fade(@color-surface-light5, 10%); + outline: none; + } + + &.epg-program-stream-row-active { + background: fade(#8b5cf6, 14%); + + &::before { + background: var(--primary-accent-color); + } + } + } + + .epg-program-stream-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.2rem; + } + + .epg-program-stream-addon { + font-size: 0.85rem; + font-weight: 600; + color: fade(@color-surface-light5, 75%); + } + + .epg-program-stream-name { + font-size: 0.95rem; + font-weight: 500; + color: var(--primary-foreground-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .epg-program-stream-meta { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem 0.65rem; + margin-top: 0.15rem; + font-size: 0.8rem; + color: fade(@color-surface-light5, 55%); + } + + .epg-program-stream-play { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + padding: 0; + border-radius: 50%; + border: none; + background-color: #00c853; + color: #fff; + cursor: pointer; + transition: transform 0.12s ease, filter 0.12s ease; + + .icon { + width: 1.35rem; + height: 1.35rem; + margin-left: 2px; + } + + &:hover, + &:focus-visible { + filter: brightness(1.08); + outline: none; + } + + &:active { + transform: scale(0.96); + } + } + + .epg-program-install { + align-self: stretch; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 0.25rem; + height: 3.25rem; + border-radius: 2rem; + padding: 0 1.25rem; + background-color: #00c853 !important; + border: none !important; + + &:hover, + &:focus-visible { + filter: brightness(1.06); + outline: var(--focus-outline-size) solid fade(#fff, 35%); + } + } + + .epg-program-install-icon { + flex: none; + width: 1.25rem; + height: 1.25rem; + color: #fff; + } + + .epg-program-install-label { + font-size: 1rem; + font-weight: 700; + color: #fff; + } +} + + diff --git a/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.tsx b/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.tsx new file mode 100644 index 000000000..117527197 --- /dev/null +++ b/src/routes/Discover/EpgGuide/EpgProgramModal/EpgProgramModal.tsx @@ -0,0 +1,161 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import classnames from 'classnames'; +import { default as Icon } from '@stremio/stremio-icons/react'; +import { ModalDialog, Button, Image } from 'stremio/components'; +import type { EPGProgram } from '../useEPG'; +import type { EpgChannel } from '../EpgGuideRow'; +import { programStartMs, programEndMs } from '../epgUtils'; +import styles from './EpgProgramModal.less'; + +export type EpgProgramStreamOption = { + id: string; + addonName: string; + name: string; + peers?: number | null; + sizeLabel?: string | null; + playHref?: string | null; +}; + +type Props = { + program: EPGProgram; + channel: EpgChannel; + streams?: EpgProgramStreamOption[]; + onCloseRequest: () => void; +}; + +function formatRuntime(program: EPGProgram): string | null { + const start = programStartMs(program); + const end = programEndMs(program); + if (isNaN(start) || isNaN(end) || end <= start) { + return null; + } + const mins = Math.round((end - start) / 60000); + if (mins < 1) { + return null; + } + if (mins < 60) { + return `${mins} min`; + } + const h = Math.floor(mins / 60); + const r = mins % 60; + return r ? `${h} h ${r} min` : `${h} h`; +} + +const EpgProgramModal = ({ program, channel, streams = [], onCloseRequest }: Props) => { + const { t } = useTranslation(); + const [selectedId, setSelectedId] = useState(null); + + useEffect(() => { + setSelectedId(streams[0]?.id ?? null); + }, [streams]); + + const runtimeLabel = useMemo(() => formatRuntime(program), [program]); + const yearLabel = useMemo(() => { + const start = programStartMs(program); + if (isNaN(start)) { + return null; + } + return String(new Date(start).getFullYear()); + }, [program]); + + const logoFallback = useCallback( + () =>
{channel.name}
, + [channel.name], + ); + + return ( + +
+
+ {channel.logo ? ( + {channel.name} + ) : ( + logoFallback() + )} +
+ + {typeof program.title === 'string' && program.title.length > 0 ? ( +
{program.title}
+ ) : null} + + {(runtimeLabel || yearLabel) && ( +
+ {runtimeLabel ? {runtimeLabel} : null} + {yearLabel ? {yearLabel} : null} +
+ )} + + {typeof program.description === 'string' && program.description.length > 0 ? ( +

{program.description}

+ ) : null} + +
+ {streams.length === 0 ? ( +
{t('NO_STREAM')}
+ ) : ( + streams.map((stream) => { + const active = stream.id === selectedId; + return ( + + ) : null} + + ); + }) + )} +
+ + +
+
+ ); +}; + +export default EpgProgramModal;