diff --git a/package-lock.json b/package-lock.json index 76db0e04a..b61d56a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@babel/preset-env": "7.16.0", "@babel/preset-react": "7.16.0", "@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", @@ -3270,6 +3271,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 bf4b1bd10..0b4fe4723 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@babel/preset-env": "7.16.0", "@babel/preset-react": "7.16.0", "@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/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..83c73b70b --- /dev/null +++ b/src/common/BottomSheet/BottomSheet.less @@ -0,0 +1,90 @@ +// 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; + + .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; + } + + .container { + z-index: 1; + position: absolute; + top: var(--horizontal-nav-bar-size); + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + gap: 1.5rem; + 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; + } +} \ 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..eed990670 --- /dev/null +++ b/src/common/BottomSheet/BottomSheet.tsx @@ -0,0 +1,83 @@ +// 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 }: 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 onClose = () => 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 = useCallback(() => { + setOffset((offset) => offset > CLOSE_THRESHOLD ? containerHeight() : 0); + setStartOffset(0); + }, []); + + const onTransitionEnd = useCallback(() => { + (offset === containerHeight()) && close(); + }, [offset]); + + useEffect(() => { + setOffset(0); + show ? open() : close(); + }, [show]); + + return opened && createPortal(( +
+
+
+
+
+
+ {title} +
+
+
+ {children} +
+
+
+ ), document.body); +}; + +export default BottomSheet; \ No newline at end of file diff --git a/src/common/BottomSheet/index.ts b/src/common/BottomSheet/index.ts new file mode 100644 index 000000000..411a57f22 --- /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; \ 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 fd4f838db..cccae3513 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 Checkbox = require('./Checkbox'); const { default: Chips } = require('./Chips'); @@ -50,6 +51,7 @@ const EventModal = require('./EventModal'); module.exports = { AddonDetailsModal, + BottomSheet, Button, Checkbox, Chips, diff --git a/src/common/useBinaryState.d.ts b/src/common/useBinaryState.d.ts new file mode 100644 index 000000000..cc1f5fccb --- /dev/null +++ b/src/common/useBinaryState.d.ts @@ -0,0 +1,8 @@ +declare const useBinaryState: () => [ + boolean, + () => void, + () => void, + () => void, +]; + +export = useBinaryState; \ No newline at end of file diff --git a/src/routes/Calendar/Calendar.tsx b/src/routes/Calendar/Calendar.tsx index 267d6f523..d51de4785 100644 --- a/src/routes/Calendar/Calendar.tsx +++ b/src/routes/Calendar/Calendar.tsx @@ -1,11 +1,13 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { useState } from 'react'; -import { MainNavBars, PaginationInput, useProfile, withCoreSuspender } from 'stremio/common'; +import React, { useMemo, useState } from 'react'; +import { MainNavBars, PaginationInput, BottomSheet, useProfile, withCoreSuspender } from 'stremio/common'; 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 useSelectableInputs from './useSelectableInputs'; import styles from './Calendar.less'; @@ -16,11 +18,14 @@ type Props = { const Calendar = ({ urlParams }: Props) => { const calendar = useCalendar(urlParams); const profile = useProfile(); - + const [paginationInput] = useSelectableInputs(calendar, profile); + const { toDayMonth } = useCalendarDate(profile); const [selected, setSelected] = useState(null); + const detailsTitle = useMemo(() => toDayMonth(selected), [selected]); + return ( { @@ -49,6 +54,12 @@ const Calendar = ({ urlParams }: Props) => { profile={profile} onChange={setSelected} /> + +
+
: diff --git a/src/routes/Calendar/Details/Details.less b/src/routes/Calendar/Details/Details.less new file mode 100644 index 000000000..db11acc78 --- /dev/null +++ b/src/routes/Calendar/Details/Details.less @@ -0,0 +1,59 @@ +// 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); + + .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..0f48b8800 --- /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; \ No newline at end of file diff --git a/src/routes/Calendar/Details/index.ts b/src/routes/Calendar/Details/index.ts new file mode 100644 index 000000000..68fcb0628 --- /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; \ No newline at end of file diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index b9cdd5507..78f4fc06b 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -105,6 +105,16 @@ } } + .more { + display: none; + flex: none; + width: 2rem; + height: 2rem; + padding: 0.5rem; + align-self: center; + color: var(--primary-foreground-color); + } + &.today { .heading { .day { @@ -128,26 +138,14 @@ } } -@media only screen and (max-height: @small) { - .cell { - gap: 1rem; - } -} - -@media only screen and (max-height: @xxsmall) { +@media only screen and (orientation: portrait) { .cell { .items { display: none; } - } -} -@media only screen and (max-width: @minimum) and (orientation: portrait) { - .cell { - .items { - .item { - border-radius: 0.25rem; - } + .more { + display: flex; } } } @@ -160,9 +158,5 @@ .heading { padding: 0; } - - .items { - display: none; - } } } diff --git a/src/routes/Calendar/Table/Cell/Cell.tsx b/src/routes/Calendar/Table/Cell/Cell.tsx index 24e6d51ec..4fc5e86c8 100644 --- a/src/routes/Calendar/Table/Cell/Cell.tsx +++ b/src/routes/Calendar/Table/Cell/Cell.tsx @@ -51,6 +51,12 @@ const Cell = ({ selected, monthInfo, date, items, onClick }: Props) => { )) } + { + items.length > 0 ? + + : + null + } ); };