diff --git a/images/calendar_placeholder.png b/images/calendar_placeholder.png new file mode 100644 index 000000000..5ae490b6c Binary files /dev/null and b/images/calendar_placeholder.png differ diff --git a/package-lock.json b/package-lock.json index 5316adf69..2edb18439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@stylistic/eslint-plugin-jsx": "^2.9.0", "@types/hat": "^0.0.4", "@types/react": "^18.2.9", + "@types/react-dom": "^18.3.0", "babel-loader": "8.2.3", "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "9.0.1", @@ -3142,6 +3143,7 @@ "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3152,7 +3154,8 @@ "node_modules/@stremio/stremio-core-web/node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/@stremio/stremio-icons": { "version": "5.4.0", @@ -3556,6 +3559,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "dev": true, diff --git a/package.json b/package.json index df491a191..2313c948f 100755 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@stylistic/eslint-plugin-jsx": "^2.9.0", "@types/hat": "^0.0.4", "@types/react": "^18.2.9", + "@types/react-dom": "^18.3.0", "babel-loader": "8.2.3", "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "9.0.1", diff --git a/src/App/routerViewsConfig.js b/src/App/routerViewsConfig.js index c80da0c94..13f331728 100644 --- a/src/App/routerViewsConfig.js +++ b/src/App/routerViewsConfig.js @@ -23,6 +23,10 @@ const routerViewsConfig = [ ...routesRegexp.library, component: routes.Library }, + { + ...routesRegexp.calendar, + component: routes.Calendar + }, { ...routesRegexp.continuewatching, component: routes.Library diff --git a/src/App/styles.less b/src/App/styles.less index be0c26480..6210a8bdf 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -91,6 +91,7 @@ html { min-height: 480px; font-family: 'PlusJakartaSans', 'sans-serif'; overflow: auto; + overscroll-behavior: none; body { width: 100%; diff --git a/src/common/BottomSheet/BottomSheet.less b/src/common/BottomSheet/BottomSheet.less new file mode 100644 index 000000000..e0d756477 --- /dev/null +++ b/src/common/BottomSheet/BottomSheet.less @@ -0,0 +1,102 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.bottom-sheet { + z-index: 99; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + + .backdrop { + z-index: 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: var(--primary-background-color); + opacity: 0.8; + transition: opacity 0.1s ease-out; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + + .container { + z-index: 1; + position: absolute; + bottom: 0; + max-height: calc(100% - var(--horizontal-nav-bar-size)); + width: 100%; + display: flex; + flex-direction: column; + gap: 1.5rem; + padding-bottom: 1rem; + border-radius: 2rem 2rem 0 0; + background-color: var(--modal-background-color); + box-shadow: var(--outer-glow); + overflow: hidden; + + &:not(.dragging) { + transition: transform 0.1s ease-out; + } + + .heading { + position: relative; + + .handle { + position: relative; + height: 2.5rem; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + + &::after { + content: ""; + height: 0.3rem; + width: 3rem; + border-radius: 1rem; + background-color: var(--primary-foreground-color); + opacity: 0.3; + } + } + + .title { + position: relative; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + padding-left: 1.5rem; + font-size: 1.25rem; + font-weight: 600; + color: var(--primary-foreground-color); + } + } + + .content { + position: relative; + overflow-y: auto; + } + } +} + +@media only screen and (min-width: @xsmall) { + .bottom-sheet { + display: none; + } +} + +@media only screen and (orientation: landscape) { + .bottom-sheet { + .container { + max-width: 90%; + } + } +} \ No newline at end of file diff --git a/src/common/BottomSheet/BottomSheet.tsx b/src/common/BottomSheet/BottomSheet.tsx new file mode 100644 index 000000000..d362aa02e --- /dev/null +++ b/src/common/BottomSheet/BottomSheet.tsx @@ -0,0 +1,87 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import classNames from 'classnames'; +import useBinaryState from 'stremio/common/useBinaryState'; +import styles from './BottomSheet.less'; + +const CLOSE_THRESHOLD = 100; + +type Props = { + children: JSX.Element, + title: string, + show?: boolean, + onClose: () => void, +}; + +const BottomSheet = ({ children, title, show, onClose }: Props) => { + const containerRef = useRef(null); + const [startOffset, setStartOffset] = useState(0); + const [offset, setOffset] = useState(0); + + const [opened, open, close] = useBinaryState(); + + const containerStyle = useMemo(() => ({ + transform: `translateY(${offset}px)` + }), [offset]); + + const containerHeight = () => containerRef.current?.offsetHeight ?? 0; + + const onCloseRequest = () => setOffset(containerHeight()); + + const onTouchStart = ({ touches }: React.TouchEvent) => { + const { clientY } = touches[0]; + setStartOffset(clientY); + }; + + const onTouchMove = useCallback(({ touches }: React.TouchEvent) => { + const { clientY } = touches[0]; + setOffset(Math.max(0, clientY - startOffset)); + }, [startOffset]); + + const onTouchEnd = () => { + setOffset((offset) => offset > CLOSE_THRESHOLD ? containerHeight() : 0); + setStartOffset(0); + }; + + const onTransitionEnd = useCallback(() => { + (offset === containerHeight()) && close(); + }, [offset]); + + useEffect(() => { + setOffset(0); + show ? open() : close(); + }, [show]); + + useEffect(() => { + !opened && onClose(); + }, [opened]); + + return opened && createPortal(( +
+
+
+
+
+
+ {title} +
+
+
+ {children} +
+
+
+ ), document.body); +}; + +export default BottomSheet; diff --git a/src/common/BottomSheet/index.ts b/src/common/BottomSheet/index.ts new file mode 100644 index 000000000..55b1638f7 --- /dev/null +++ b/src/common/BottomSheet/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import BottomSheet from './BottomSheet'; +export default BottomSheet; diff --git a/src/common/Chips/Chip/Chip.less b/src/common/Chips/Chip/Chip.less index 2adbcb727..b71a7b6b6 100644 --- a/src/common/Chips/Chip/Chip.less +++ b/src/common/Chips/Chip/Chip.less @@ -20,14 +20,16 @@ background-color: transparent; user-select: none; overflow: hidden; + opacity: 0.6; &:hover { background-color: var(--overlay-color); transition: background-color 0.1s ease-out; + opacity: 1; } &.active { - font-weight: 700; + opacity: 1; background-color: var(--quaternary-accent-color); transition: background-color 0.1s ease-in; } diff --git a/src/common/Chips/Chips.less b/src/common/Chips/Chips.less index cf0e85917..7d7e15d18 100644 --- a/src/common/Chips/Chips.less +++ b/src/common/Chips/Chips.less @@ -1,7 +1,5 @@ // Copyright (C) 2017-2024 Smart code 203358507 -@mask-width: 10%; - .chips { position: relative; width: 100%; @@ -9,17 +7,4 @@ align-items: center; justify-content: flex-start; gap: 1rem; - overflow-x: auto; - - &.left { - mask-image: linear-gradient(90deg, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%); - } - - &.right { - mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width); - } - - &.center { - mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%); - } } \ No newline at end of file diff --git a/src/common/Chips/Chips.tsx b/src/common/Chips/Chips.tsx index 8804775b0..3d8f4a120 100644 --- a/src/common/Chips/Chips.tsx +++ b/src/common/Chips/Chips.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { memo, useEffect, useRef, useState } from 'react'; -import classNames from 'classnames'; +import React, { memo } from 'react'; +import HorizontalScroll from '../HorizontalScroll'; import Chip from './Chip'; import styles from './Chips.less'; @@ -16,28 +16,9 @@ type Props = { onSelect: (value: string) => {}, }; -const SCROLL_THRESHOLD = 1; - const Chips = memo(({ options, selected, onSelect }: Props) => { - const ref = useRef(null); - const [scrollPosition, setScrollPosition] = useState('left'); - - useEffect(() => { - const onScroll = ({ target }: Event) => { - const { scrollLeft, scrollWidth, offsetWidth} = target as HTMLDivElement; - const position = - (scrollLeft - SCROLL_THRESHOLD) <= 0 ? 'left' : - (scrollLeft + offsetWidth + SCROLL_THRESHOLD) >= scrollWidth ? 'right' : - 'center'; - setScrollPosition(position); - }; - - ref.current?.addEventListener('scroll', onScroll); - return () => ref.current?.removeEventListener('scroll', onScroll); - }, []); - return ( -
+ { options.map(({ label, value }) => ( { /> )) } -
+ ); }); diff --git a/src/common/HorizontalScroll/HorizontalScroll.less b/src/common/HorizontalScroll/HorizontalScroll.less new file mode 100644 index 000000000..cb4b9be7c --- /dev/null +++ b/src/common/HorizontalScroll/HorizontalScroll.less @@ -0,0 +1,20 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@mask-width: 10%; + +.horizontal-scroll { + position: relative; + overflow-x: auto; + + &.left { + mask-image: linear-gradient(90deg, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%); + } + + &.right { + mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width); + } + + &.center { + mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%); + } +} diff --git a/src/common/HorizontalScroll/HorizontalScroll.tsx b/src/common/HorizontalScroll/HorizontalScroll.tsx new file mode 100644 index 000000000..b4c23b19b --- /dev/null +++ b/src/common/HorizontalScroll/HorizontalScroll.tsx @@ -0,0 +1,40 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useRef, useEffect, useState } from 'react'; +import classNames from 'classnames'; +import styles from './HorizontalScroll.less'; + +const SCROLL_THRESHOLD = 1; + +type Props = { + className: string, + children: React.ReactNode, +}; + +const HorizontalScroll = ({ className, children }: Props) => { + const ref = useRef(null); + const [scrollPosition, setScrollPosition] = useState('left'); + + useEffect(() => { + const onScroll = ({ target }: Event) => { + const { scrollLeft, scrollWidth, offsetWidth } = target as HTMLDivElement; + + setScrollPosition(() => ( + (scrollLeft - SCROLL_THRESHOLD) <= 0 ? 'left' : + (scrollLeft + offsetWidth + SCROLL_THRESHOLD) >= scrollWidth ? 'right' : + 'center' + )); + }; + + ref.current?.addEventListener('scroll', onScroll); + return () => ref.current?.removeEventListener('scroll', onScroll); + }, []); + + return ( +
+ {children} +
+ ); +}; + +export default HorizontalScroll; diff --git a/src/common/HorizontalScroll/index.ts b/src/common/HorizontalScroll/index.ts new file mode 100644 index 000000000..4fc875461 --- /dev/null +++ b/src/common/HorizontalScroll/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import HorizontalScroll from './HorizontalScroll'; +export default HorizontalScroll; diff --git a/src/common/MainNavBars/MainNavBars.js b/src/common/MainNavBars/MainNavBars.js index 542d68fe4..3ccf200ec 100644 --- a/src/common/MainNavBars/MainNavBars.js +++ b/src/common/MainNavBars/MainNavBars.js @@ -10,6 +10,7 @@ const TABS = [ { id: 'board', label: 'Board', icon: 'home', href: '#/' }, { id: 'discover', label: 'Discover', icon: 'discover', href: '#/discover' }, { id: 'library', label: 'Library', icon: 'library', href: '#/library' }, + { id: 'calendar', label: 'Calendar', icon: 'calendar', href: '#/calendar' }, { id: 'addons', label: 'ADDONS', icon: 'addons', href: '#/addons' }, { id: 'settings', label: 'SETTINGS', icon: 'settings', href: '#/settings' }, ]; diff --git a/src/common/NavBar/VerticalNavBar/styles.less b/src/common/NavBar/VerticalNavBar/styles.less index 99e59be93..3a6ae76d8 100644 --- a/src/common/NavBar/VerticalNavBar/styles.less +++ b/src/common/NavBar/VerticalNavBar/styles.less @@ -9,6 +9,7 @@ align-items: center; gap: 1rem; width: var(--vertical-nav-bar-size); + padding: 1rem 0; background-color: transparent; overflow-y: auto; scrollbar-width: none; @@ -18,16 +19,8 @@ } .nav-tab-button { - width: calc(var(--vertical-nav-bar-size) - 1.5rem); - height: calc(var(--vertical-nav-bar-size) - 1.5rem); - - &:first-child { - margin-top: 1rem; - } - - &:last-child { - margin-bottom: 1rem; - } + width: calc(var(--vertical-nav-bar-size) - 1.2rem); + height: calc(var(--vertical-nav-bar-size) - 1.2rem); } } @@ -45,12 +38,18 @@ .nav-tab-button { flex: none; - &:first-child { - margin-top: 0; - } - &:last-child { - margin-bottom: 0; + display: none; + } + } + } +} + +@media only screen and (max-height: @minimum) { + .vertical-nav-bar-container { + .nav-tab-button { + &:last-child { + display: none; } } } diff --git a/src/common/PaginationInput/PaginationInput.js b/src/common/PaginationInput/PaginationInput.js deleted file mode 100644 index 41d740b3f..000000000 --- a/src/common/PaginationInput/PaginationInput.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const { default: Icon } = require('@stremio/stremio-icons/react'); -const Button = require('stremio/common/Button'); -const styles = require('./styles'); - -const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => { - const prevNextButtonOnClick = React.useCallback((event) => { - if (typeof onSelect === 'function') { - onSelect({ - type: 'change-page', - value: event.currentTarget.dataset.value, - dataset: dataset, - reactEvent: event, - nativeEvent: event.nativeEvent - }); - } - }, [dataset, onSelect]); - return ( -
- -
-
{label}
-
- -
- ); -}; - -PaginationInput.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - dataset: PropTypes.object, - onSelect: PropTypes.func -}; - -module.exports = PaginationInput; diff --git a/src/common/PaginationInput/index.js b/src/common/PaginationInput/index.js deleted file mode 100644 index da139e27d..000000000 --- a/src/common/PaginationInput/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const PaginationInput = require('./PaginationInput'); - -module.exports = PaginationInput; diff --git a/src/common/PaginationInput/styles.less b/src/common/PaginationInput/styles.less deleted file mode 100644 index ecf6c56a4..000000000 --- a/src/common/PaginationInput/styles.less +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; - -.pagination-input-container { - display: flex; - flex-direction: row; - border-radius: var(--border-radius); - - .prev-button-container, .next-button-container { - flex: none; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--overlay-color); - - .icon { - display: block; - color: var(--primary-foreground-color); - } - } - - .label-container { - flex: 1; - align-self: stretch; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--overlay-color); - - .label { - flex: none; - min-width: 1.2rem; - max-width: 3rem; - white-space: nowrap; - text-overflow: ellipsis; - text-align: center; - font-weight: 500; - color: var(--primary-foreground-color); - } - } -} \ No newline at end of file diff --git a/src/common/animations.less b/src/common/animations.less index 8a7fc2b9e..4b0776d16 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -19,4 +19,23 @@ opacity: 1; transform: translateY(0); } +} + +:global(.animation-slide-up) { + :local { + animation-name: slide-up; + } + + animation-timing-function: ease-out; + animation-duration: 0.1s; +} + +@keyframes slide-up { + 0% { + transform: translateY(100%); + } + + 100% { + transform: translateY(0%); + } } \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 6387821f0..720226eb8 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const AddonDetailsModal = require('./AddonDetailsModal'); +const { default: BottomSheet } = require('./BottomSheet'); const Button = require('./Button'); const Toggle = require('./Toggle'); const { default: Chips } = require('./Chips'); @@ -17,7 +18,7 @@ const ModalDialog = require('./ModalDialog'); const Multiselect = require('./Multiselect'); const { default: MultiselectMenu } = require('./MultiselectMenu'); const { HorizontalNavBar, VerticalNavBar } = require('./NavBar'); -const PaginationInput = require('./PaginationInput'); +const { default: HorizontalScroll } = require('./HorizontalScroll'); const { PlatformProvider, usePlatform } = require('./Platform'); const PlayIconCircleCentered = require('./PlayIconCircleCentered'); const Popup = require('./Popup'); @@ -51,6 +52,7 @@ const { default: Checkbox } = require('./Checkbox'); module.exports = { AddonDetailsModal, + BottomSheet, Button, Toggle, Chips, @@ -67,8 +69,8 @@ module.exports = { Multiselect, MultiselectMenu, HorizontalNavBar, + HorizontalScroll, VerticalNavBar, - PaginationInput, PlatformProvider, usePlatform, PlayIconCircleCentered, diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index ca5efdb2c..3903da44b 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -17,6 +17,10 @@ const routesRegexp = { regexp: /^\/library(?:\/([^/]*))?$/, urlParamsNames: ['type'] }, + calendar: { + regexp: /^\/calendar(?:\/([^/]*)\/([^/]*))?$/, + urlParamsNames: ['year', 'month'] + }, continuewatching: { regexp: /^\/continuewatching(?:\/([^/]*))?$/, urlParamsNames: ['type'] diff --git a/src/modules.d.ts b/src/modules.d.ts index 66f93be4c..b3eb8813f 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -3,4 +3,5 @@ declare module '*.less' { export = resource; } +declare module 'stremio/common'; declare module 'stremio/common/Button'; diff --git a/src/routes/Calendar/Calendar.less b/src/routes/Calendar/Calendar.less new file mode 100644 index 000000000..7f7925bf5 --- /dev/null +++ b/src/routes/Calendar/Calendar.less @@ -0,0 +1,43 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.calendar { + width: 100%; + height: 100%; + background-color: transparent; + + .content { + position: relative; + display: flex; + flex-direction: row; + gap: 0.5rem; + width: 100%; + height: 100%; + padding: 0 0 2rem 2rem; + + .main { + flex: auto; + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + } + } +} + +@media only screen and (max-width: @minimum) { + .calendar { + .content { + padding: 0; + } + } +} + +@media only screen and (max-width: @small) and (orientation: landscape) { + .calendar { + .content { + padding: 0 0 0 1rem; + } + } +} diff --git a/src/routes/Calendar/Calendar.tsx b/src/routes/Calendar/Calendar.tsx new file mode 100644 index 000000000..2ff1da70a --- /dev/null +++ b/src/routes/Calendar/Calendar.tsx @@ -0,0 +1,75 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo, useState } from 'react'; +import { MainNavBars, BottomSheet, useProfile, withCoreSuspender } from 'stremio/common'; +import Selector from './Selector'; +import Table from './Table'; +import List from './List'; +import Details from './Details'; +import Placeholder from './Placeholder'; +import useCalendar from './useCalendar'; +import useCalendarDate from './useCalendarDate'; +import styles from './Calendar.less'; + +type Props = { + urlParams: UrlParams, +}; + +const Calendar = ({ urlParams }: Props) => { + const calendar = useCalendar(urlParams); + const profile = useProfile(); + + const { toDayMonth } = useCalendarDate(profile); + + const [selected, setSelected] = useState(null); + + const detailsTitle = useMemo(() => toDayMonth(selected), [selected, toDayMonth]); + + const onDetailsClose = () => { + setSelected(null); + }; + + return ( + + { + profile.auth !== null ? +
+
+ + + + + +
+ + + : + + } + + ); +}; + +const CalendarFallback = () => ( + +); + +export default withCoreSuspender(Calendar, CalendarFallback); diff --git a/src/routes/Calendar/Details/Details.less b/src/routes/Calendar/Details/Details.less new file mode 100644 index 000000000..2e78ca703 --- /dev/null +++ b/src/routes/Calendar/Details/Details.less @@ -0,0 +1,60 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.details { + position: relative; + + .video { + flex: none; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0 1.5rem; + height: 4rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + -webkit-tap-highlight-color: transparent; + + .name { + flex: auto; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .info { + flex: none; + display: block; + } + + .icon { + flex: none; + width: 2rem; + height: 2rem; + padding: 0.5rem; + border-radius: 50%; + color: var(--primary-foreground-color); + } + + &:hover, &:active { + background-color: var(--overlay-color); + + .icon { + display: block; + background-color: var(--secondary-accent-color); + } + } + } + + .placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 10rem; + font-size: 1rem; + color: var(--primary-foreground-color); + } +} \ No newline at end of file diff --git a/src/routes/Calendar/Details/Details.tsx b/src/routes/Calendar/Details/Details.tsx new file mode 100644 index 000000000..a99768eb7 --- /dev/null +++ b/src/routes/Calendar/Details/Details.tsx @@ -0,0 +1,45 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import Icon from '@stremio/stremio-icons/react'; +import Button from 'stremio/common/Button'; +import styles from './Details.less'; + +type Props = { + selected: CalendarDate | null, + items: CalendarItem[], +}; + +const Details = ({ selected, items }: Props) => { + const videos = useMemo(() => { + return items.find(({ date }) => date.day === selected?.day)?.items ?? []; + }, [selected, items]); + + return ( +
+ { + videos.map(({ id, name, season, episode, deepLinks }) => ( + + )) + } + { + !videos.length ? +
+ No new episodes for this day +
+ : + null + } +
+ ); +}; + +export default Details; diff --git a/src/routes/Calendar/Details/index.ts b/src/routes/Calendar/Details/index.ts new file mode 100644 index 000000000..5d0205438 --- /dev/null +++ b/src/routes/Calendar/Details/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Details from './Details'; +export default Details; diff --git a/src/routes/Calendar/List/Item/Item.less b/src/routes/Calendar/List/Item/Item.less new file mode 100644 index 000000000..aba6bba09 --- /dev/null +++ b/src/routes/Calendar/List/Item/Item.less @@ -0,0 +1,98 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.item { + flex: none; + position: relative; + display: flex; + flex-direction: column; + background-color: var(--overlay-color); + border-radius: var(--border-radius); + border: 0.15rem solid transparent; + transition: border-color 0.1s ease-out; + + .heading { + flex: none; + position: relative; + display: flex; + align-items: center; + height: 3.5rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + padding: 0 1rem; + } + + .body { + flex: auto; + display: flex; + flex-direction: column; + + .video { + flex: none; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: 3rem; + padding: 0 1rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + + &:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); + } + + .name { + flex: auto; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .info { + flex: none; + display: block; + } + + .icon { + flex: none; + display: none; + width: 2rem; + height: 2rem; + padding: 0.5rem; + border-radius: 50%; + color: var(--primary-foreground-color); + background-color: var(--secondary-accent-color); + } + + &:hover { + background-color: var(--overlay-color); + + .info { + display: none; + } + + .icon { + display: block; + } + } + } + } + + &.today { + .heading { + background-color: var(--primary-accent-color); + } + } + + &.active { + border-color: var(--primary-foreground-color); + } + + &:not(.active):hover { + border-color: var(--overlay-color); + } +} diff --git a/src/routes/Calendar/List/Item/Item.tsx b/src/routes/Calendar/List/Item/Item.tsx new file mode 100644 index 000000000..70010b0f5 --- /dev/null +++ b/src/routes/Calendar/List/Item/Item.tsx @@ -0,0 +1,68 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useEffect, useMemo, useRef } from 'react'; +import Icon from '@stremio/stremio-icons/react'; +import classNames from 'classnames'; +import { Button } from 'stremio/common'; +import useCalendarDate from '../../useCalendarDate'; +import styles from './Item.less'; + +type Props = { + selected: CalendarDate | null, + monthInfo: CalendarMonthInfo, + date: CalendarDate, + items: CalendarContentItem[], + profile: Profile, + onClick: (date: CalendarDate) => void, +}; + +const Item = ({ selected, monthInfo, date, items, profile, onClick }: Props) => { + const ref = useRef(null); + const { toDayMonth } = useCalendarDate(profile); + + const [active, today] = useMemo(() => [ + date.day === selected?.day, + date.day === monthInfo.today, + ], [selected, monthInfo, date]); + + const onItemClick = () => { + onClick && onClick(date); + }; + + useEffect(() => { + active && ref.current?.scrollIntoView({ + block: 'start', + behavior: 'smooth', + }); + }, [active]); + + return ( +
+
+ {toDayMonth(date)} +
+
+ { + items.map(({ id, name, season, episode, deepLinks }) => ( + + )) + } +
+
+ ); +}; + +export default Item; diff --git a/src/routes/Calendar/List/Item/index.ts b/src/routes/Calendar/List/Item/index.ts new file mode 100644 index 000000000..1cf96beb5 --- /dev/null +++ b/src/routes/Calendar/List/Item/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Item from './Item'; + +export default Item; diff --git a/src/routes/Calendar/List/List.less b/src/routes/Calendar/List/List.less new file mode 100644 index 000000000..f63078680 --- /dev/null +++ b/src/routes/Calendar/List/List.less @@ -0,0 +1,37 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.list { + flex: none; + display: flex; + flex-direction: column; + gap: 1rem; + width: 20rem; + padding: 0 1rem; + overflow-y: auto; +} + +@media only screen and (max-width: @small) and (orientation: portrait) { + .list { + display: none; + } +} + +@media only screen and (max-width: @medium) and (orientation: landscape) { + .list { + width: 20rem; + } +} + +@media only screen and (max-width: @small) and (orientation: landscape) { + .list { + width: 17rem; + } +} + +@media only screen and (max-width: @xsmall) and (orientation: landscape) { + .list { + display: none; + } +} diff --git a/src/routes/Calendar/List/List.tsx b/src/routes/Calendar/List/List.tsx new file mode 100644 index 000000000..13745e380 --- /dev/null +++ b/src/routes/Calendar/List/List.tsx @@ -0,0 +1,38 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import Item from './Item'; +import styles from './List.less'; + +type Props = { + items: CalendarItem[], + selected: CalendarDate | null, + monthInfo: CalendarMonthInfo, + profile: Profile, + onChange: (date: CalendarDate) => void, +}; + +const List = ({ items, selected, monthInfo, profile, onChange }: Props) => { + const filteredItems = useMemo(() => { + return items.filter(({ items }) => items.length); + }, [items]); + + return ( +
+ { + filteredItems.map((item) => ( + + )) + } +
+ ); +}; + +export default List; diff --git a/src/routes/Calendar/List/index.ts b/src/routes/Calendar/List/index.ts new file mode 100644 index 000000000..7990b192e --- /dev/null +++ b/src/routes/Calendar/List/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import List from './List'; + +export default List; diff --git a/src/routes/Calendar/Placeholder/Placeholder.less b/src/routes/Calendar/Placeholder/Placeholder.less new file mode 100644 index 000000000..a509ff79e --- /dev/null +++ b/src/routes/Calendar/Placeholder/Placeholder.less @@ -0,0 +1,99 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.placeholder { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + overflow-y: auto; + + .title { + flex: none; + font-size: 1.75rem; + font-weight: 400; + text-align: center; + color: var(--primary-foreground-color); + margin-bottom: 1rem; + opacity: 0.5; + } + + .image { + flex: none; + height: 14rem; + margin: 1.5rem 0; + } + + .overview { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 4rem; + margin-bottom: 3rem; + + .point { + display: flex; + flex-direction: row; + align-items: center; + gap: 1.5rem; + width: 18rem; + + .icon { + flex: none; + height: 3.25rem; + width: 3.25rem; + color: var(--primary-foreground-color); + opacity: 0.3; + } + + .text { + flex: auto; + font-size: 1.1rem; + font-size: 500; + color: var(--primary-foreground-color); + opacity: 0.9; + } + } + } + + .button { + flex: none; + justify-content: center; + height: 4rem; + line-height: 4rem; + padding: 0 5rem; + font-size: 1.1rem; + color: var(--primary-foreground-color); + text-align: center; + border-radius: 3.5rem; + background-color: var(--overlay-color); + + &:hover { + outline: var(--focus-outline-size) solid var(--primary-foreground-color); + background-color: transparent; + } + } +} + +@media only screen and (max-width: @minimum) { + .placeholder { + padding: 1rem 2rem; + + .image { + height: 10rem; + } + + .overview { + flex-direction: column; + } + + .button { + width: 100%; + } + } +} \ No newline at end of file diff --git a/src/routes/Calendar/Placeholder/Placeholder.tsx b/src/routes/Calendar/Placeholder/Placeholder.tsx new file mode 100644 index 000000000..feb236221 --- /dev/null +++ b/src/routes/Calendar/Placeholder/Placeholder.tsx @@ -0,0 +1,43 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Icon from '@stremio/stremio-icons/react'; +import { Button, Image } from 'stremio/common'; +import styles from './Placeholder.less'; + +const Placeholder = () => { + const { t } = useTranslation(); + + return ( +
+
+ {t('CALENDAR_NOT_LOGGED_IN')} +
+ +
+
+ +
+ {t('NOT_LOGGED_IN_NOTIFICATIONS')} +
+
+
+ +
+ {t('NOT_LOGGED_IN_CALENDAR')} +
+
+
+ +
+ ); +}; + +export default Placeholder; diff --git a/src/routes/Calendar/Placeholder/index.ts b/src/routes/Calendar/Placeholder/index.ts new file mode 100644 index 000000000..786a2e9da --- /dev/null +++ b/src/routes/Calendar/Placeholder/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Placeholder from './Placeholder'; + +export default Placeholder; diff --git a/src/routes/Calendar/Selector/Selector.less b/src/routes/Calendar/Selector/Selector.less new file mode 100644 index 000000000..6683697be --- /dev/null +++ b/src/routes/Calendar/Selector/Selector.less @@ -0,0 +1,92 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.selector { + flex: none; + position: relative; + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + padding: 0 1rem; + + .prev, .next { + position: relative; + height: 3rem; + width: 6rem; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + border-radius: 0.5rem; + transition: background-color 0.1s ease-out; + -webkit-tap-highlight-color: transparent; + + .label, .icon { + color: var(--primary-foreground-color); + opacity: 0.5; + transition: opacity 0.1s ease-out; + } + + .label { + font-size: 1rem; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .icon { + height: 1.5rem; + } + + &:hover { + .label, .icon { + opacity: 1; + } + + background-color: var(--overlay-color); + } + } + + .prev { + padding-left: 0.5rem; + padding-right: 1.25rem; + } + + .next { + padding-left: 1.25rem; + padding-right: 0.5rem; + } + + .selected { + position: relative; + width: 8.5rem; + text-align: center; + + .year { + font-size: 1rem; + font-weight: 500; + line-height: 100%; + color: var(--primary-foreground-color); + opacity: 0.5; + } + + .month { + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } +} + +@media only screen and (max-width: @small) { + .selector { + justify-content: space-between; + } +} \ No newline at end of file diff --git a/src/routes/Calendar/Selector/Selector.tsx b/src/routes/Calendar/Selector/Selector.tsx new file mode 100644 index 000000000..d6aa23336 --- /dev/null +++ b/src/routes/Calendar/Selector/Selector.tsx @@ -0,0 +1,62 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useCallback, useMemo } from 'react'; +import Icon from '@stremio/stremio-icons/react'; +import { Button } from 'stremio/common'; +import useCalendarDate from '../useCalendarDate'; +import styles from './Selector.less'; + +type Props = { + selected: CalendarSelected, + selectable: CalendarSelectable, + profile: Profile, +}; + +const Selector = ({ selected, selectable, profile }: Props) => { + const { toMonth } = useCalendarDate(profile); + + const [prev, next] = useMemo(() => ( + [selectable.prev, selectable.next] + ), [selectable]); + + const onPrev = useCallback(() => { + window.location.href = prev.deepLinks.calendar; + }, [prev]); + + const onNext = useCallback(() => { + window.location.href = next.deepLinks.calendar; + }, [next]); + + return ( +
+ +
+
+ {selected?.year} +
+
+ {toMonth(selected, 'long')} +
+
+ +
+ ); +}; + +export default Selector; diff --git a/src/routes/Calendar/Selector/index.ts b/src/routes/Calendar/Selector/index.ts new file mode 100644 index 000000000..f8baa472a --- /dev/null +++ b/src/routes/Calendar/Selector/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Selector from './Selector'; +export default Selector; diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less new file mode 100644 index 000000000..1d54855bc --- /dev/null +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -0,0 +1,177 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.cell { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 0.5rem; + background-color: var(--overlay-color); + border: 0.15rem solid transparent; + overflow: hidden; + cursor: pointer; + transition: border-color 0.1s ease-out; + -webkit-tap-highlight-color: transparent; + + &:first-child { + border-radius: var(--border-radius) 0 0 0; + } + + &:nth-child(7) { + border-radius: 0 var(--border-radius) 0 0; + } + + &:last-child { + border-radius: 0 0 var(--border-radius) 0; + } + + .heading { + flex: none; + position: relative; + height: 3rem; + display: flex; + align-items: center; + padding: 0 1rem; + + .day { + flex: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 2rem; + width: 2rem; + border-radius: 100%; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + } + } + + .items { + flex: 0 1 10rem; + position: relative; + display: flex; + flex-direction: row; + gap: 1rem; + padding: 0 1rem 1rem 1rem; + + .item { + flex: none; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + aspect-ratio: 2 / 3; + border-radius: var(--border-radius); + + .icon { + flex: none; + z-index: 1; + position: absolute; + width: 2rem; + height: 2rem; + padding: 0.5rem; + border-radius: 50%; + color: var(--primary-foreground-color); + background-color: var(--secondary-accent-color); + opacity: 0; + } + + .poster { + flex: auto; + z-index: 0; + position: relative; + height: 100%; + width: 100%; + object-fit: cover; + opacity: 1; + } + + .icon, .poster { + transition: opacity 0.1s ease-out; + } + + &:hover { + .icon { + opacity: 1; + } + + .poster { + opacity: 0.5; + } + } + } + } + + .more { + display: none; + flex: none; + width: 2rem; + height: 2rem; + padding: 0.5rem; + align-self: center; + color: var(--primary-foreground-color); + } + + &.today { + .heading { + .day { + background-color: var(--primary-accent-color); + } + } + } + + &.active { + border-color: var(--primary-foreground-color); + } + + &:not(.active):hover { + border-color: var(--overlay-color); + } +} + +@media only screen and (orientation: portrait) { + .cell { + .heading { + justify-content: center; + } + + .items { + display: none; + } + + .more { + display: flex; + } + } +} + +@media only screen and (max-width: @small) and (orientation: landscape) { + .cell { + flex-direction: row; + align-items: center; + + .items { + display: none; + } + + .more { + display: flex; + } + } +} + +@media only screen and (max-height: @xxsmall) and (orientation: landscape) { + .cell { + .items { + display: none; + } + + .more { + display: flex; + } + } +} diff --git a/src/routes/Calendar/Table/Cell/Cell.tsx b/src/routes/Calendar/Table/Cell/Cell.tsx new file mode 100644 index 000000000..b5e78464c --- /dev/null +++ b/src/routes/Calendar/Table/Cell/Cell.tsx @@ -0,0 +1,61 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import Icon from '@stremio/stremio-icons/react'; +import classNames from 'classnames'; +import { Button, Image, HorizontalScroll } from 'stremio/common'; +import styles from './Cell.less'; + +type Props = { + selected: CalendarDate | null, + monthInfo: CalendarMonthInfo, + date: CalendarDate, + items: CalendarContentItem[], + onClick: (date: CalendarDate) => void, +}; + +const Cell = ({ selected, monthInfo, date, items, onClick }: Props) => { + const [active, today] = useMemo(() => [ + date.day === selected?.day, + date.day === monthInfo.today, + ], [selected, monthInfo, date]); + + const onCellClick = () => { + onClick && onClick(date); + }; + + return ( + + )) + } + + { + items.length > 0 ? + + : + null + } + + ); +}; + +export default Cell; diff --git a/src/routes/Calendar/Table/Cell/index.ts b/src/routes/Calendar/Table/Cell/index.ts new file mode 100644 index 000000000..0581ae97a --- /dev/null +++ b/src/routes/Calendar/Table/Cell/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Cell from './Cell'; + +export default Cell; diff --git a/src/routes/Calendar/Table/Table.less b/src/routes/Calendar/Table/Table.less new file mode 100644 index 000000000..65a9b01e9 --- /dev/null +++ b/src/routes/Calendar/Table/Table.less @@ -0,0 +1,65 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.table { + flex: auto; + position: relative; + display: flex; + flex-direction: column; + + .week { + flex: none; + position: relative; + height: 3rem; + width: 100%; + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + + .day { + position: relative; + padding: 0.5rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + .long { + display: block; + } + + .short { + display: none; + } + } + } + + .grid { + flex: auto; + position: relative; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 1px; + } +} + +@media only screen and (max-width: @xsmall) { + .table { + .week { + .day { + .long { + display: none; + } + + .short { + display: block; + } + } + } + } +} \ No newline at end of file diff --git a/src/routes/Calendar/Table/Table.tsx b/src/routes/Calendar/Table/Table.tsx new file mode 100644 index 000000000..0a7bce335 --- /dev/null +++ b/src/routes/Calendar/Table/Table.tsx @@ -0,0 +1,62 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './Table.less'; +import Cell from './Cell/Cell'; + +const WEEK_DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + +type Props = { + items: CalendarItem[], + selected: CalendarDate | null, + monthInfo: CalendarMonthInfo, + onChange: (date: CalendarDate) => void, +}; + +const Table = ({ items, selected, monthInfo, onChange }: Props) => { + const { t } = useTranslation(); + + const cellsOffset = useMemo(() => { + return Array.from(Array(monthInfo.firstWeekday).keys()); + }, [monthInfo]); + + return ( +
+
+ { + WEEK_DAYS.map((day) => ( +
+ + {t(day)} + + + {t(day).slice(0, 3)} + +
+ )) + } +
+
+ { + cellsOffset.map((day) => ( + + )) + } + { + items.map((item) => ( + + )) + } +
+
+ ); +}; + +export default Table; diff --git a/src/routes/Calendar/Table/index.ts b/src/routes/Calendar/Table/index.ts new file mode 100644 index 000000000..b1000a3c1 --- /dev/null +++ b/src/routes/Calendar/Table/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Table from './Table'; + +export default Table; diff --git a/src/routes/Calendar/index.ts b/src/routes/Calendar/index.ts new file mode 100644 index 000000000..3a1f44b68 --- /dev/null +++ b/src/routes/Calendar/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Calendar from './Calendar'; + +export default Calendar; diff --git a/src/routes/Calendar/useCalendar.ts b/src/routes/Calendar/useCalendar.ts new file mode 100644 index 000000000..55ffc5d07 --- /dev/null +++ b/src/routes/Calendar/useCalendar.ts @@ -0,0 +1,27 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import { useModelState } from 'stremio/common'; + +const useCalendar = (urlParams: UrlParams) => { + const action = React.useMemo(() => { + const args = urlParams.year && urlParams.month ? { + year: parseInt(urlParams.year), + month: parseInt(urlParams.month), + day: urlParams.day ? parseInt(urlParams.day) : null, + } : null; + + return { + action: 'Load', + args: { + model: 'Calendar', + args, + }, + }; + }, [urlParams]); + + const calendar = useModelState({ model: 'calendar', action }) as Calendar; + return calendar; +}; + +export default useCalendar; diff --git a/src/routes/Calendar/useCalendarDate.ts b/src/routes/Calendar/useCalendarDate.ts new file mode 100644 index 000000000..082098380 --- /dev/null +++ b/src/routes/Calendar/useCalendarDate.ts @@ -0,0 +1,50 @@ +import { useCallback } from 'react'; + +const useCalendarDate = (profile: Profile) => { + const toMonth = useCallback((calendarDate: CalendarDate | CalendarSelectableDate | null, format: 'short' | 'long'): string => { + if (!calendarDate) return ''; + + const date = new Date(); + date.setDate(1); + date.setMonth(calendarDate.month - 1); + + return date.toLocaleString(profile.settings.interfaceLanguage, { + month: format, + }); + }, [profile.settings]); + + const toMonthYear = useCallback((calendarDate: CalendarDate | null): string => { + if (!calendarDate) return ''; + + const date = new Date(); + date.setDate(1); + date.setMonth(calendarDate.month - 1); + date.setFullYear(calendarDate.year); + + return date.toLocaleString(profile.settings.interfaceLanguage, { + month: 'long', + year: 'numeric', + }); + }, [profile.settings]); + + const toDayMonth = useCallback((calendarDate: CalendarDate | null): string => { + if (!calendarDate) return ''; + + const date = new Date(); + date.setDate(calendarDate.day); + date.setMonth(calendarDate.month - 1); + + return date.toLocaleString(profile.settings.interfaceLanguage, { + day: 'numeric', + month: 'short', + }); + }, [profile.settings]); + + return { + toMonth, + toMonthYear, + toDayMonth, + }; +}; + +export default useCalendarDate; diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 1d8bca9cb..3af4ddce9 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -11,13 +11,6 @@ multiselect-label: label; } -:import('~stremio/common/PaginationInput/styles.less') { - pagination-prev-button-container: prev-button-container; - pagination-next-button-container: next-button-container; - pagination-button-icon: icon; - pagination-label: label; -} - :import('~stremio/common/ModalDialog/styles.less') { selectable-inputs-modal-container: modal-dialog-container; selectable-inputs-modal-content: modal-dialog-content; diff --git a/src/routes/index.js b/src/routes/index.js index 47a2eacd8..076a2213d 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,6 +4,7 @@ const Addons = require('./Addons'); const Board = require('./Board'); const Discover = require('./Discover'); const Library = require('./Library'); +const Calendar = require('./Calendar').default; const MetaDetails = require('./MetaDetails'); const NotFound = require('./NotFound'); const Search = require('./Search'); @@ -16,6 +17,7 @@ module.exports = { Board, Discover, Library, + Calendar, MetaDetails, NotFound, Search, diff --git a/src/services/KeyboardShortcuts/KeyboardShortcuts.js b/src/services/KeyboardShortcuts/KeyboardShortcuts.js index 773abf23f..22ef0e41f 100644 --- a/src/services/KeyboardShortcuts/KeyboardShortcuts.js +++ b/src/services/KeyboardShortcuts/KeyboardShortcuts.js @@ -35,10 +35,15 @@ function KeyboardShortcuts() { } case 'Digit4': { event.preventDefault(); - window.location = '#/addons'; + window.location = '#/calendar'; break; } case 'Digit5': { + event.preventDefault(); + window.location = '#/addons'; + break; + } + case 'Digit6': { event.preventDefault(); window.location = '#/settings'; break; diff --git a/src/types/models/Calendar.d.ts b/src/types/models/Calendar.d.ts new file mode 100644 index 000000000..5aeff8eb3 --- /dev/null +++ b/src/types/models/Calendar.d.ts @@ -0,0 +1,55 @@ +type CalendarDeepLinks = { + calendar: string, +}; + +type CalendarItemDeepLinks = { + metaDetailsStreams: string, +}; + +type CalendarSelectableDate = { + month: number, + year: number, + selected: boolean, + deepLinks: CalendarDeepLinks, +}; + +type CalendarSelectable = { + prev: CalendarSelectableDate, + next: CalendarSelectableDate, +}; + +type CalendarDate = { + day: number, + month: number, + year: number, +}; + +type CalendarSelected = CalendarDate | null; + +type CalendarMonthInfo = { + today: number | null, + days: number, + firstWeekday: number, +}; + +type CalendarContentItem = { + id: string, + name: string, + poster?: string, + title: string, + season?: number, + episode?: number, + deepLinks: CalendarItemDeepLinks, +}; + +type CalendarItem = { + date: CalendarDate, + items: CalendarContentItem[], +}; + +type Calendar = { + selectable: CalendarSelectable, + selected: CalendarSelected, + monthInfo: CalendarMonthInfo, + items: CalendarItem[], +};