diff --git a/web/.eslintrc b/web/.eslintrc index 5ff8941..344d47b 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -12,6 +12,7 @@ "endOfLine": "crlf" }], "import/no-anonymous-default-export": 0, // Allow "export default" + "import/prefer-default-export": 0, "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}], "react/jsx-one-expression-per-line": 0, "import/order": ["warn", { diff --git a/web/README.md b/web/README.md index e037846..6ffd9c3 100644 --- a/web/README.md +++ b/web/README.md @@ -15,4 +15,7 @@ > Prettier will fix the code every time the code is saved - `yarn lint` - to find all linting problems -- `yarn fix` - to fix code \ No newline at end of file +- `yarn fix` - to fix code + +### How images were generated +`npx pwa-asset-generator public/logo.png public -m public/site.webmanifest -p "calc(50vh - 25%) calc(50vw - 25%)" -b "linear-gradient(135deg, rgb(50,54,55), rgb(84,90,94))" -q 100 -i public/index.html -f` \ No newline at end of file diff --git a/web/public/android-chrome-192x192.png b/web/public/android-chrome-192x192.png deleted file mode 100644 index 57d8237..0000000 Binary files a/web/public/android-chrome-192x192.png and /dev/null differ diff --git a/web/public/apple-icon-180.png b/web/public/apple-icon-180.png new file mode 100644 index 0000000..c94132d Binary files /dev/null and b/web/public/apple-icon-180.png differ diff --git a/web/public/apple-splash-1125-2436.jpg b/web/public/apple-splash-1125-2436.jpg new file mode 100644 index 0000000..204e567 Binary files /dev/null and b/web/public/apple-splash-1125-2436.jpg differ diff --git a/web/public/apple-splash-1136-640.jpg b/web/public/apple-splash-1136-640.jpg new file mode 100644 index 0000000..7d61b40 Binary files /dev/null and b/web/public/apple-splash-1136-640.jpg differ diff --git a/web/public/apple-splash-1170-2532.jpg b/web/public/apple-splash-1170-2532.jpg new file mode 100644 index 0000000..5ce2d09 Binary files /dev/null and b/web/public/apple-splash-1170-2532.jpg differ diff --git a/web/public/apple-splash-1242-2208.jpg b/web/public/apple-splash-1242-2208.jpg new file mode 100644 index 0000000..2ff6755 Binary files /dev/null and b/web/public/apple-splash-1242-2208.jpg differ diff --git a/web/public/apple-splash-1242-2688.jpg b/web/public/apple-splash-1242-2688.jpg new file mode 100644 index 0000000..e495b42 Binary files /dev/null and b/web/public/apple-splash-1242-2688.jpg differ diff --git a/web/public/apple-splash-1284-2778.jpg b/web/public/apple-splash-1284-2778.jpg new file mode 100644 index 0000000..00cd3b7 Binary files /dev/null and b/web/public/apple-splash-1284-2778.jpg differ diff --git a/web/public/apple-splash-1334-750.jpg b/web/public/apple-splash-1334-750.jpg new file mode 100644 index 0000000..33886a2 Binary files /dev/null and b/web/public/apple-splash-1334-750.jpg differ diff --git a/web/public/apple-splash-1536-2048.jpg b/web/public/apple-splash-1536-2048.jpg new file mode 100644 index 0000000..ed75557 Binary files /dev/null and b/web/public/apple-splash-1536-2048.jpg differ diff --git a/web/public/apple-splash-1620-2160.jpg b/web/public/apple-splash-1620-2160.jpg new file mode 100644 index 0000000..345c52f Binary files /dev/null and b/web/public/apple-splash-1620-2160.jpg differ diff --git a/web/public/apple-splash-1668-2224.jpg b/web/public/apple-splash-1668-2224.jpg new file mode 100644 index 0000000..a958370 Binary files /dev/null and b/web/public/apple-splash-1668-2224.jpg differ diff --git a/web/public/apple-splash-1668-2388.jpg b/web/public/apple-splash-1668-2388.jpg new file mode 100644 index 0000000..413f9cc Binary files /dev/null and b/web/public/apple-splash-1668-2388.jpg differ diff --git a/web/public/apple-splash-1792-828.jpg b/web/public/apple-splash-1792-828.jpg new file mode 100644 index 0000000..21856f1 Binary files /dev/null and b/web/public/apple-splash-1792-828.jpg differ diff --git a/web/public/apple-splash-2048-1536.jpg b/web/public/apple-splash-2048-1536.jpg new file mode 100644 index 0000000..1e22899 Binary files /dev/null and b/web/public/apple-splash-2048-1536.jpg differ diff --git a/web/public/apple-splash-2048-2732.jpg b/web/public/apple-splash-2048-2732.jpg new file mode 100644 index 0000000..a8b662a Binary files /dev/null and b/web/public/apple-splash-2048-2732.jpg differ diff --git a/web/public/apple-splash-2160-1620.jpg b/web/public/apple-splash-2160-1620.jpg new file mode 100644 index 0000000..07b2c38 Binary files /dev/null and b/web/public/apple-splash-2160-1620.jpg differ diff --git a/web/public/apple-splash-2208-1242.jpg b/web/public/apple-splash-2208-1242.jpg new file mode 100644 index 0000000..a7388c5 Binary files /dev/null and b/web/public/apple-splash-2208-1242.jpg differ diff --git a/web/public/apple-splash-2224-1668.jpg b/web/public/apple-splash-2224-1668.jpg new file mode 100644 index 0000000..5b4eab4 Binary files /dev/null and b/web/public/apple-splash-2224-1668.jpg differ diff --git a/web/public/apple-splash-2388-1668.jpg b/web/public/apple-splash-2388-1668.jpg new file mode 100644 index 0000000..afb10cc Binary files /dev/null and b/web/public/apple-splash-2388-1668.jpg differ diff --git a/web/public/apple-splash-2436-1125.jpg b/web/public/apple-splash-2436-1125.jpg new file mode 100644 index 0000000..04d2513 Binary files /dev/null and b/web/public/apple-splash-2436-1125.jpg differ diff --git a/web/public/apple-splash-2532-1170.jpg b/web/public/apple-splash-2532-1170.jpg new file mode 100644 index 0000000..599fbe1 Binary files /dev/null and b/web/public/apple-splash-2532-1170.jpg differ diff --git a/web/public/apple-splash-2688-1242.jpg b/web/public/apple-splash-2688-1242.jpg new file mode 100644 index 0000000..50f0a89 Binary files /dev/null and b/web/public/apple-splash-2688-1242.jpg differ diff --git a/web/public/apple-splash-2732-2048.jpg b/web/public/apple-splash-2732-2048.jpg new file mode 100644 index 0000000..370034a Binary files /dev/null and b/web/public/apple-splash-2732-2048.jpg differ diff --git a/web/public/apple-splash-2778-1284.jpg b/web/public/apple-splash-2778-1284.jpg new file mode 100644 index 0000000..99a3192 Binary files /dev/null and b/web/public/apple-splash-2778-1284.jpg differ diff --git a/web/public/apple-splash-640-1136.jpg b/web/public/apple-splash-640-1136.jpg new file mode 100644 index 0000000..3ef6078 Binary files /dev/null and b/web/public/apple-splash-640-1136.jpg differ diff --git a/web/public/apple-splash-750-1334.jpg b/web/public/apple-splash-750-1334.jpg new file mode 100644 index 0000000..7e1d9f1 Binary files /dev/null and b/web/public/apple-splash-750-1334.jpg differ diff --git a/web/public/apple-splash-828-1792.jpg b/web/public/apple-splash-828-1792.jpg new file mode 100644 index 0000000..f4d8c35 Binary files /dev/null and b/web/public/apple-splash-828-1792.jpg differ diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png deleted file mode 100644 index ae07815..0000000 Binary files a/web/public/apple-touch-icon.png and /dev/null differ diff --git a/web/public/dlnaicon-120.jpg b/web/public/dlnaicon-120.jpg deleted file mode 100644 index a64a809..0000000 Binary files a/web/public/dlnaicon-120.jpg and /dev/null differ diff --git a/web/public/dlnaicon-120.png b/web/public/dlnaicon-120.png deleted file mode 100644 index bf163a6..0000000 Binary files a/web/public/dlnaicon-120.png and /dev/null differ diff --git a/web/public/dlnaicon-48.jpg b/web/public/dlnaicon-48.jpg deleted file mode 100644 index ac70fe2..0000000 Binary files a/web/public/dlnaicon-48.jpg and /dev/null differ diff --git a/web/public/dlnaicon-48.png b/web/public/dlnaicon-48.png deleted file mode 100644 index fbead48..0000000 Binary files a/web/public/dlnaicon-48.png and /dev/null differ diff --git a/web/public/favicon-16x16.png b/web/public/favicon-16x16.png deleted file mode 100644 index 27a879a..0000000 Binary files a/web/public/favicon-16x16.png and /dev/null differ diff --git a/web/public/favicon-196.png b/web/public/favicon-196.png new file mode 100644 index 0000000..969911b Binary files /dev/null and b/web/public/favicon-196.png differ diff --git a/web/public/favicon-32x32.png b/web/public/favicon-32x32.png deleted file mode 100644 index 9c339e0..0000000 Binary files a/web/public/favicon-32x32.png and /dev/null differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico deleted file mode 100644 index a2658a8..0000000 Binary files a/web/public/favicon.ico and /dev/null differ diff --git a/web/public/index.html b/web/public/index.html index 484c1c3..df1c16c 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,44 +1,66 @@ - - - - - - - - - - - - - - - - TorrServer MatriX - - - - -
- - - - - - - - + + + + + + + + + + + TorrServer MatriX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/web/public/android-chrome-512x512.png b/web/public/logo.png similarity index 100% rename from web/public/android-chrome-512x512.png rename to web/public/logo.png diff --git a/web/public/manifest-icon-192.maskable.png b/web/public/manifest-icon-192.maskable.png new file mode 100644 index 0000000..ec114c2 Binary files /dev/null and b/web/public/manifest-icon-192.maskable.png differ diff --git a/web/public/manifest-icon-512.maskable.png b/web/public/manifest-icon-512.maskable.png new file mode 100644 index 0000000..3da40e5 Binary files /dev/null and b/web/public/manifest-icon-512.maskable.png differ diff --git a/web/public/mstile-150x150.png b/web/public/mstile-150x150.png deleted file mode 100644 index c5158b2..0000000 Binary files a/web/public/mstile-150x150.png and /dev/null differ diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest index b20abb7..6ba810e 100644 --- a/web/public/site.webmanifest +++ b/web/public/site.webmanifest @@ -1,19 +1,33 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "TorrServer", + "short_name": "TorrServer", + "icons": [ + { + "src": "manifest-icon-192.maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "manifest-icon-192.maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "manifest-icon-512.maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "manifest-icon-512.maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/web/src/components/About/index.jsx b/web/src/components/About/index.jsx index 180cad2..5255422 100644 --- a/web/src/components/About/index.jsx +++ b/web/src/components/About/index.jsx @@ -1,14 +1,15 @@ import axios from 'axios' import { useEffect, useState } from 'react' import Button from '@material-ui/core/Button' -import Dialog from '@material-ui/core/Dialog' import InfoIcon from '@material-ui/icons/Info' -import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import { useTranslation } from 'react-i18next' import { useMediaQuery } from '@material-ui/core' import { echoHost } from 'utils/Hosts' +import { StyledDialog, StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' +import { isStandaloneApp } from 'utils/Utils' +import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' import LinkComponent from './LinkComponent' import { DialogWrapper, HeaderSection, ThanksSection, Section, FooterSection } from './style' @@ -22,27 +23,41 @@ export default function AboutDialog() { axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data)) }, []) + const onClose = () => setOpen(false) + const ref = useOnStandaloneAppOutsideClick(onClose) + return ( <> - setOpen(true)}> - - - - - + setOpen(true)}> + {isStandaloneApp ? ( + <> + +
{t('Details')}
+ + ) : ( + <> + + + - + + )} +
+ + setOpen(false)} + onClose={onClose} aria-labelledby='form-dialog-title' fullScreen={fullScreen} maxWidth='xl' + ref={ref} >
{t('About')}
{torrServerVersion} - ts-icon + ts-icon
@@ -72,12 +87,12 @@ export default function AboutDialog() {
-
- +
) } diff --git a/web/src/components/About/style.js b/web/src/components/About/style.js index 827bd84..903bad6 100644 --- a/web/src/components/About/style.js +++ b/web/src/components/About/style.js @@ -1,4 +1,5 @@ import styled, { css } from 'styled-components' +import { standaloneMedia } from 'style/standaloneMedia' export const DialogWrapper = styled.div` height: 100%; @@ -26,6 +27,10 @@ export const HeaderSection = styled.section` width: 60px; } } + + ${standaloneMedia(css` + padding-top: 30px; + `)} ` export const ThanksSection = styled.section` diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index 01e146f..2469e2b 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import Button from '@material-ui/core/Button' -import Dialog from '@material-ui/core/Dialog' import { torrentsHost, torrentUploadHost } from 'utils/Hosts' import axios from 'axios' import { useTranslation } from 'react-i18next' @@ -12,7 +11,9 @@ import usePreviousState from 'utils/usePreviousState' import { useQuery } from 'react-query' import { getTorrents } from 'utils/Utils' import parseTorrent from 'parse-torrent' -import { ButtonWrapper, Header } from 'style/DialogStyles' +import { ButtonWrapper } from 'style/DialogStyles' +import { StyledDialog, StyledHeader } from 'style/CustomMaterialUiStyles' +import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' import { checkImageURL, getMoviePosters, checkTorrentSource, parseTorrentTitle } from './helpers' import { Content } from './style' @@ -46,6 +47,8 @@ export default function AddDialog({ const [isCustomTitleEnabled, setIsCustomTitleEnabled] = useState(false) const [currentSourceHash, setCurrentSourceHash] = useState() + const ref = useOnStandaloneAppOutsideClick(handleClose) + const { data: torrents } = useQuery('torrents', getTorrents, { retry: 1, refetchInterval: 1000 }) useEffect(() => { @@ -223,8 +226,8 @@ export default function AddDialog({ } return ( - -
{t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')}
+ + {t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')} {!isEditMode && ( @@ -279,6 +282,6 @@ export default function AddDialog({ {isSaving ? : t(isEditMode ? 'Save' : 'Add')} -
+ ) } diff --git a/web/src/components/Add/index.jsx b/web/src/components/Add/index.jsx index 7c2d027..e845824 100644 --- a/web/src/components/Add/index.jsx +++ b/web/src/components/Add/index.jsx @@ -2,10 +2,12 @@ import { useState } from 'react' import ListItemIcon from '@material-ui/core/ListItemIcon' import LibraryAddIcon from '@material-ui/icons/LibraryAdd' import ListItemText from '@material-ui/core/ListItemText' -import ListItem from '@material-ui/core/ListItem' import { useTranslation } from 'react-i18next' +import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' +import { isStandaloneApp } from 'utils/Utils' import AddDialog from './AddDialog' +import { StyledPWAAddButton } from './style' export default function AddDialogButton({ isOffline, isLoading }) { const { t } = useTranslation() @@ -15,12 +17,19 @@ export default function AddDialogButton({ isOffline, isLoading }) { return (
- - - - - - + + {isStandaloneApp ? ( + + ) : ( + <> + + + + + + + )} + {isDialogOpen && }
diff --git a/web/src/components/Add/style.js b/web/src/components/Add/style.js index 76fdbc1..45422bf 100644 --- a/web/src/components/Add/style.js +++ b/web/src/components/Add/style.js @@ -95,6 +95,7 @@ export const LeftSideBottomSectionNoFile = styled.div` ${LeftSideBottomSectionBasicStyles} border: 4px dashed rgba(0,0,0,0.1); text-align: center; + outline: none; ${({ isDragActive }) => isDragActive && `border: 4px dashed green`}; @@ -336,3 +337,30 @@ export const PosterLanguageSwitch = styled.div` } `} ` + +export const StyledPWAAddButton = styled.div` + border: 2px solid white; + border-radius: 50%; + height: 45px; + width: 45px; + position: relative; + + :before, + :after { + content: ''; + background: white; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + :before { + width: 2px; + height: 25px; + } + :after { + width: 25px; + height: 2px; + } +` diff --git a/web/src/components/App/PWAFooter/index.jsx b/web/src/components/App/PWAFooter/index.jsx new file mode 100644 index 0000000..a953a38 --- /dev/null +++ b/web/src/components/App/PWAFooter/index.jsx @@ -0,0 +1,31 @@ +import { CreditCard as CreditCardIcon } from '@material-ui/icons' +import { useTranslation } from 'react-i18next' +import CloseServer from 'components/CloseServer' +import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' +import AddDialogButton from 'components/Add' +import AboutDialog from 'components/About' +import SettingsDialogButton from 'components/Settings' + +import StyledPWAFooter from './style' + +export default function PWAFooter({ setIsDonationDialogOpen, isOffline, isLoading }) { + const { t } = useTranslation() + + return ( + + + + setIsDonationDialogOpen(true)}> + + +
{t('Donate')}
+
+ + + + + + +
+ ) +} diff --git a/web/src/components/App/PWAFooter/style.js b/web/src/components/App/PWAFooter/style.js new file mode 100644 index 0000000..1bf4b8f --- /dev/null +++ b/web/src/components/App/PWAFooter/style.js @@ -0,0 +1,21 @@ +import { standaloneMedia } from 'style/standaloneMedia' +import styled, { css } from 'styled-components' + +export const pwaFooterHeight = 90 + +export default styled.div` + background: #575757; + color: #fff; + position: fixed; + bottom: 0; + width: 100%; + height: ${pwaFooterHeight}px; + + display: none; + + ${standaloneMedia(css` + display: grid; + grid-template-columns: repeat(5, calc(100% / 5)); + justify-items: center; + `)} +` diff --git a/web/src/components/App/PWAInstallationGuide/IOSShareIcon.jsx b/web/src/components/App/PWAInstallationGuide/IOSShareIcon.jsx new file mode 100644 index 0000000..aa02156 --- /dev/null +++ b/web/src/components/App/PWAInstallationGuide/IOSShareIcon.jsx @@ -0,0 +1,21 @@ +export default function IOSShareIcon() { + return ( + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + + + + ) +} diff --git a/web/src/components/App/PWAInstallationGuide/index.jsx b/web/src/components/App/PWAInstallationGuide/index.jsx new file mode 100644 index 0000000..ae84217 --- /dev/null +++ b/web/src/components/App/PWAInstallationGuide/index.jsx @@ -0,0 +1,57 @@ +import IconButton from '@material-ui/core/IconButton' +import CloseIcon from '@material-ui/icons/Close' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import IOSShareIcon from './IOSShareIcon' +import { StyledWrapper, StyledHeader, StyledContent } from './style' + +export function PWAInstallationGuide() { + const pwaNotificationIsClosed = JSON.parse(localStorage.getItem('pwaNotificationIsClosed')) + const [isOpen, setIsOpen] = useState(!pwaNotificationIsClosed) + const [shouldBeOpened, setShouldBeOpened] = useState(!pwaNotificationIsClosed) + + const { t } = useTranslation() + + if (!isOpen) return null + + return ( + + + ts-icon + + {t('PWAGuide.Header')} + + { + setShouldBeOpened(false) + + setTimeout(() => { + setIsOpen(false) + localStorage.setItem('pwaNotificationIsClosed', true) + }, 300) + }} + > + + + + + +

{t('PWAGuide.Description')}

+ +

{t('PWAGuide.VLC')}

+ +

+ 1. {t('PWAGuide.FirstStep')} +

+ +

+ 2. {t('PWAGuide.SecondStep.Select')} {t('PWAGuide.SecondStep.AddToHomeScreen')} +

+
+
+ ) +} diff --git a/web/src/components/App/PWAInstallationGuide/style.jsx b/web/src/components/App/PWAInstallationGuide/style.jsx new file mode 100644 index 0000000..1c9b730 --- /dev/null +++ b/web/src/components/App/PWAInstallationGuide/style.jsx @@ -0,0 +1,59 @@ +import styled, { css } from 'styled-components' + +export const StyledWrapper = styled.div` + ${({ isOpen }) => css` + position: absolute; + bottom: 10px; + left: 50%; + background: #eeeef0; + width: calc(100% - 20px); + z-index: 9999; + border-radius: 10px; + transition: all 0.3s; + color: #000; + + ${isOpen + ? css` + opacity: 1; + transform: translate(-50%, 0); + ` + : css` + transform: translate(-50%, 150%); + opacity: 0; + pointer-events: none; + `} + + > :not(:last-child) { + border-bottom: 1px solid #dadadc; + } + + > * { + padding: 20px; + } + `} +` + +export const StyledHeader = styled.div` + display: grid; + grid-auto-flow: column; + grid-template-columns: min-content 1fr; + gap: 20px; + align-items: center; + font-weight: 700; + + img { + border-radius: 5px; + } +` + +export const StyledContent = styled.div` + > :not(:last-child) { + margin-bottom: 25px; + } + + span { + background: #fefcfd; + padding: 5px; + border-radius: 5px; + } +` diff --git a/web/src/components/App/index.jsx b/web/src/components/App/index.jsx index cbee0c9..83dbd7d 100644 --- a/web/src/components/App/index.jsx +++ b/web/src/components/App/index.jsx @@ -1,7 +1,6 @@ import CssBaseline from '@material-ui/core/CssBaseline' import { createContext, useEffect, useState } from 'react' import Typography from '@material-ui/core/Typography' -import IconButton from '@material-ui/core/IconButton' import { Menu as MenuIcon, Close as CloseIcon, @@ -19,13 +18,18 @@ import useChangeLanguage from 'utils/useChangeLanguage' import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles' import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components' import { useQuery } from 'react-query' -import { getTorrents } from 'utils/Utils' +import { getTorrents, isStandaloneApp } from 'utils/Utils' import GlobalStyle from 'style/GlobalStyle' +import { lightTheme, THEME_MODES, useMaterialUITheme } from 'style/materialUISetup' +import getStyledComponentsTheme from 'style/getStyledComponentsTheme' +import checkIsIOS from 'utils/checkIsIOS' -import { AppWrapper, AppHeader, HeaderToggle } from './style' +import { AppWrapper, AppHeader, HeaderToggle, StyledIconButton } from './style' import Sidebar from './Sidebar' -import { lightTheme, THEME_MODES, useMaterialUITheme } from '../../style/materialUISetup' -import getStyledComponentsTheme from '../../style/getStyledComponentsTheme' +import PWAFooter from './PWAFooter' +import { PWAInstallationGuide } from './PWAInstallationGuide' + +const snackbarIsClosed = JSON.parse(localStorage.getItem('snackbarIsClosed')) export const DarkModeContext = createContext() @@ -63,14 +67,9 @@ export default function App() { - setIsDrawerOpen(!isDrawerOpen)} - style={{ marginRight: '6px' }} - > + setIsDrawerOpen(!isDrawerOpen)}> {isDrawerOpen ? : } - + TorrServer {torrServerVersion} @@ -118,11 +117,17 @@ export default function App() { + + {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} - {!JSON.parse(localStorage.getItem('snackbarIsClosed')) && } + {snackbarIsClosed ? checkIsIOS() && !isStandaloneApp && : } diff --git a/web/src/components/App/style.js b/web/src/components/App/style.js index 7deba4e..fe63790 100644 --- a/web/src/components/App/style.js +++ b/web/src/components/App/style.js @@ -1,6 +1,10 @@ +import { IconButton } from '@material-ui/core' import { rgba } from 'polished' +import { standaloneMedia } from 'style/standaloneMedia' import styled, { css } from 'styled-components' +import { pwaFooterHeight } from './PWAFooter/style' + export const AppWrapper = styled.div` ${({ theme: { @@ -15,13 +19,23 @@ export const AppWrapper = styled.div` grid-template-areas: 'head head' 'side content'; + + ${standaloneMedia(css` + grid-template-columns: 0 1fr; + grid-template-rows: ${pwaFooterHeight}px 1fr ${pwaFooterHeight}px; + height: 100vh; + `)} `} ` export const CenteredGrid = styled.div` - height: 100%; display: grid; place-items: center; + + ${standaloneMedia(css` + height: 100vh; + width: 100vw; + `)} ` export const AppHeader = styled.div` @@ -36,6 +50,15 @@ export const AppHeader = styled.div` box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); padding: 0 16px; z-index: 3; + + ${standaloneMedia(css` + grid-template-columns: max-content 1fr; + align-items: end; + padding: 7px 16px; + position: fixed; + width: 100%; + height: ${pwaFooterHeight}px; + `)} `} ` export const AppSidebarStyle = styled.div` @@ -58,6 +81,10 @@ export const AppSidebarStyle = styled.div` svg { fill: ${sidebarFillColor}; } + + ${standaloneMedia(css` + display: none; + `)} `} ` export const TorrentListWrapper = styled.div` @@ -83,6 +110,11 @@ export const TorrentListWrapper = styled.div` @media (max-width: 700px) { grid-template-columns: 1fr; } + + ${standaloneMedia(css` + height: calc(100vh - ${pwaFooterHeight}px); + padding-bottom: 105px; + `)} ` export const HeaderToggle = styled.div` @@ -117,3 +149,11 @@ export const HeaderToggle = styled.div` } `} ` + +export const StyledIconButton = styled(IconButton)` + margin-right: 6px; + + ${standaloneMedia(css` + display: none; + `)} +` diff --git a/web/src/components/CloseServer.jsx b/web/src/components/CloseServer.jsx index a347eeb..160c1d9 100644 --- a/web/src/components/CloseServer.jsx +++ b/web/src/components/CloseServer.jsx @@ -1,8 +1,11 @@ import { useState } from 'react' -import { Button, Dialog, DialogActions, DialogTitle, ListItem, ListItemIcon, ListItemText } from '@material-ui/core' +import { Button, DialogActions, DialogTitle, ListItemIcon, ListItemText } from '@material-ui/core' +import { StyledDialog, StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' import { PowerSettingsNew as PowerSettingsNewIcon } from '@material-ui/icons' import { shutdownHost } from 'utils/Hosts' import { useTranslation } from 'react-i18next' +import { isStandaloneApp } from 'utils/Utils' +import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' export default function CloseServer({ isOffline, isLoading }) { const { t } = useTranslation() @@ -10,17 +13,28 @@ export default function CloseServer({ isOffline, isLoading }) { const closeDialog = () => setOpen(false) const openDialog = () => setOpen(true) + const ref = useOnStandaloneAppOutsideClick(closeDialog) + return ( <> - - - - + + {isStandaloneApp ? ( + <> + +
{t('TurnOff')}
+ + ) : ( + <> + + + - -
+ + + )} + - + {t('CloseServer?')} - + ) } diff --git a/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx index 08290ec..dd28d41 100644 --- a/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx +++ b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx @@ -1,9 +1,10 @@ import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' import { ArrowBack } from '@material-ui/icons' +import { isStandaloneApp } from 'utils/Utils' const useStyles = makeStyles({ - appBar: { position: 'relative' }, + appBar: { position: 'relative', ...(isStandaloneApp && { paddingTop: '30px' }) }, title: { marginLeft: '5px', flex: 1 }, }) diff --git a/web/src/components/DialogTorrentDetailsContent/Table/index.jsx b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx index 8a15bb1..dd65182 100644 --- a/web/src/components/DialogTorrentDetailsContent/Table/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx @@ -28,6 +28,8 @@ const Table = memo( // if files in list is more then 1 and no season text detected by ptt.parse, show full name const shouldDisplayFullFileName = playableFileList.length > 1 && !fileHasEpisodeText + const isVlcUsed = JSON.parse(localStorage.getItem('isVlcUsed')) ?? true + return !playableFileList?.length ? ( 'No playable files in this torrent' ) : ( @@ -133,11 +135,19 @@ const Table = memo( {t('Preload')} - - - + {isVlcUsed ? ( + + + + ) : ( + + + + )} - + ) } diff --git a/web/src/components/Donate/index.jsx b/web/src/components/Donate/index.jsx index c689e4a..4ec9bfa 100644 --- a/web/src/components/Donate/index.jsx +++ b/web/src/components/Donate/index.jsx @@ -5,9 +5,15 @@ import IconButton from '@material-ui/core/IconButton' import CreditCardIcon from '@material-ui/icons/CreditCard' import CloseIcon from '@material-ui/icons/Close' import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { standaloneMedia } from 'style/standaloneMedia' import DonateDialog from './DonateDialog' +const StyledSnackbar = styled(Snackbar)` + ${standaloneMedia('margin-bottom: 90px')}; +` + export default function DonateSnackbar() { const { t } = useTranslation() const [open, setOpen] = useState(false) @@ -22,7 +28,7 @@ export default function DonateSnackbar() { <> {open && setOpen(false)} />} - + {t('SettingsDialog.MobileAppSettings')} + + setIsVlcUsed(prev => !prev)} color='secondary' />} + label={t('SettingsDialog.UseVLC')} + labelPlacement='start' + /> + + ) +} diff --git a/web/src/components/Settings/SettingsDialog.jsx b/web/src/components/Settings/SettingsDialog.jsx index 56fe3cb..2058d99 100644 --- a/web/src/components/Settings/SettingsDialog.jsx +++ b/web/src/components/Settings/SettingsDialog.jsx @@ -1,5 +1,4 @@ import axios from 'axios' -import Dialog from '@material-ui/core/Dialog' import Button from '@material-ui/core/Button' import Checkbox from '@material-ui/core/Checkbox' import { FormControlLabel, useMediaQuery, useTheme } from '@material-ui/core' @@ -11,12 +10,16 @@ import Tabs from '@material-ui/core/Tabs' import Tab from '@material-ui/core/Tab' import SwipeableViews from 'react-swipeable-views' import CircularProgress from '@material-ui/core/CircularProgress' +import { StyledDialog } from 'style/CustomMaterialUiStyles' +import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' +import { isStandaloneApp } from 'utils/Utils' import { SettingsHeader, FooterSection, Content } from './style' import defaultSettings from './defaultSettings' import { a11yProps, TabPanel } from './tabComponents' import PrimarySettingsComponent from './PrimarySettingsComponent' import SecondarySettingsComponent from './SecondarySettingsComponent' +import MobileAppSettings from './MobileAppSettings' export default function SettingsDialog({ handleClose }) { const { t } = useTranslation() @@ -29,6 +32,7 @@ export default function SettingsDialog({ handleClose }) { const [cachePercentage, setCachePercentage] = useState(40) const [preloadCachePercentage, setPreloadCachePercentage] = useState(0) const [isProMode, setIsProMode] = useState(JSON.parse(localStorage.getItem('isProMode')) || false) + const [isVlcUsed, setIsVlcUsed] = useState(JSON.parse(localStorage.getItem('isVlcUsed')) ?? true) useEffect(() => { axios.post(settingsHost(), { action: 'get' }).then(({ data }) => { @@ -36,6 +40,8 @@ export default function SettingsDialog({ handleClose }) { }) }, []) + const ref = useOnStandaloneAppOutsideClick(handleClose) + const handleSave = () => { handleClose() const sets = JSON.parse(JSON.stringify(settings)) @@ -43,6 +49,7 @@ export default function SettingsDialog({ handleClose }) { sets.ReaderReadAHead = cachePercentage sets.PreloadCache = preloadCachePercentage axios.post(settingsHost(), { action: 'set', sets }) + localStorage.setItem('isVlcUsed', isVlcUsed) } const inputForm = ({ target: { type, value, checked, id } }) => { @@ -82,7 +89,7 @@ export default function SettingsDialog({ handleClose }) { const handleChangeIndex = index => setSelectedTab(index) return ( - +
{t('SettingsDialog.Settings')}
+ + {isStandaloneApp && } @@ -150,6 +159,12 @@ export default function SettingsDialog({ handleClose }) { + + {isStandaloneApp && ( + + + + )} ) : ( @@ -179,6 +194,6 @@ export default function SettingsDialog({ handleClose }) { {t('Save')} -
+ ) } diff --git a/web/src/components/Settings/index.jsx b/web/src/components/Settings/index.jsx index 5073946..aa006b4 100644 --- a/web/src/components/Settings/index.jsx +++ b/web/src/components/Settings/index.jsx @@ -1,9 +1,10 @@ -import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import { useState } from 'react' import SettingsIcon from '@material-ui/icons/Settings' import { useTranslation } from 'react-i18next' +import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' +import { isStandaloneApp } from 'utils/Utils' import SettingsDialog from './SettingsDialog' @@ -16,12 +17,22 @@ export default function SettingsDialogButton({ isOffline, isLoading }) { return (
- - - - - - + + {isStandaloneApp ? ( + <> + +
{t('SettingsDialog.Settings')}
+ + ) : ( + <> + + + + + + + )} +
{isDialogOpen && }
diff --git a/web/src/components/Settings/style.js b/web/src/components/Settings/style.js index 5c80d88..87d6fdf 100644 --- a/web/src/components/Settings/style.js +++ b/web/src/components/Settings/style.js @@ -1,11 +1,11 @@ import styled, { css } from 'styled-components' import { mainColors } from 'style/colors' -import { Header } from 'style/DialogStyles' +import { StyledHeader } from 'style/CustomMaterialUiStyles' export const cacheBeforeReaderColor = '#b3dfc9' export const cacheAfterReaderColor = mainColors.light.primary -export const SettingsHeader = styled(Header)` +export const SettingsHeader = styled(StyledHeader)` display: grid; grid-auto-flow: column; align-items: center; diff --git a/web/src/components/TorrentCard/index.jsx b/web/src/components/TorrentCard/index.jsx index 97044da..8d53e9f 100644 --- a/web/src/components/TorrentCard/index.jsx +++ b/web/src/components/TorrentCard/index.jsx @@ -16,6 +16,8 @@ import axios from 'axios' import ptt from 'parse-torrent-title' import { useTranslation } from 'react-i18next' import AddDialog from 'components/Add/AddDialog' +import { StyledDialog } from 'style/CustomMaterialUiStyles' +import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style' @@ -61,6 +63,8 @@ const Torrent = ({ torrent }) => { const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(parsedTitle || 'file')}.m3u?link=${hash}&m3u` + const detailedInfoDialogRef = useOnStandaloneAppOutsideClick(closeDetailedInfo) + return ( <> @@ -121,16 +125,17 @@ const Torrent = ({ torrent }) => { - - + {t('DeleteTorrent?')} diff --git a/web/src/components/TorrentList/style.js b/web/src/components/TorrentList/style.js index 3332421..12a3dbc 100644 --- a/web/src/components/TorrentList/style.js +++ b/web/src/components/TorrentList/style.js @@ -1,7 +1,12 @@ import styled, { css } from 'styled-components' export default styled.div` - ${({ isButton }) => css` + ${({ + isButton, + theme: { + addDialog: { notificationSuccessBGColor, languageSwitchBGColor }, + }, + }) => css` display: grid; place-items: center; padding: 20px 40px; @@ -9,12 +14,12 @@ export default styled.div` ${isButton && css` - background: #88cdaa; + background: ${notificationSuccessBGColor}; transition: 0.2s; cursor: pointer; :hover { - background: #74c39c; + background: ${languageSwitchBGColor}; } `} diff --git a/web/src/locales/en/translation.json b/web/src/locales/en/translation.json index 2b005ba..e66b3ef 100644 --- a/web/src/locales/en/translation.json +++ b/web/src/locales/en/translation.json @@ -75,6 +75,16 @@ "Playlist": "Playlist", "Preload": "Preload", "ProjectSource": "Project page", + "PWAGuide": { + "Header": "Install application", + "Description": "Install the app on your device to easily access it anytime. No app store. No download.", + "VLC": "VLC button will be added to open video instantly on the phone", + "FirstStep": "Tap on", + "SecondStep": { + "Select": "Select", + "AddToHomeScreen":"Add to Home Screen" + } + }, "Releases": "TorrServer Releases", "RemoveAll": "Remove All", "RemoveViews": "Remove View States", @@ -88,6 +98,7 @@ "SettingsDialog": { "AddRetrackers": "Add retrackers", "AdditionalSettings": "Additional Settings", + "MobileAppSettings": "Mobile app settings", "CacheBeforeReaderDesc": "from cache will be saved before currently played frame", "CacheAfterReaderDesc": "from cache will be loaded after currently played frame", "CacheSize": "Cache Size", @@ -125,8 +136,10 @@ "Tabs": { "Main": "Main", "Additional": "Additional", - "AdditionalDisabled": "(enable PRO mode)" - } + "AdditionalDisabled": "(enable PRO mode)", + "App": "App" + }, + "UseVLC": "Prompt to open video in VLC" }, "Size": "Size", "SpecialThanks": "Special Thanks", diff --git a/web/src/locales/ru/translation.json b/web/src/locales/ru/translation.json index b7d27ec..1956065 100644 --- a/web/src/locales/ru/translation.json +++ b/web/src/locales/ru/translation.json @@ -75,6 +75,16 @@ "Playlist": "Плейлист", "Preload": "Предзагр.", "ProjectSource": "Сайт проекта", + "PWAGuide": { + "Header": "Установить приложение", + "Description": "Установите приложение на ваше устройство для быстрого доступа в любой момент. Без AppStore. Без загрузки.", + "VLC": "VLC кнопка будет добавлена для мгновенного воспроизведения на телефоне", + "FirstStep": "Нажмите на", + "SecondStep": { + "Select": "Выбирите", + "AddToHomeScreen":"На экран «Домой»" + } + }, "Releases": "Релизы TorrServer", "RemoveAll": "Удалить все", "RemoveViews": "Очистить просмотры", @@ -88,6 +98,7 @@ "SettingsDialog": { "AddRetrackers": "Добавлять", "AdditionalSettings": "Дополнительные настройки", + "MobileAppSettings": "Настройки моб. приложения", "CacheBeforeReaderDesc": "от кеша будет оставаться позади воспроизводимого кадра", "CacheAfterReaderDesc": "кеша будет спереди от воспроизводимого кадра", "CacheSize": "Размер кеша", @@ -125,8 +136,10 @@ "Tabs": { "Main": "Основные", "Additional": "Дополнительные", - "AdditionalDisabled": "(включите ПРО-режим)" - } + "AdditionalDisabled": "(включите ПРО-режим)", + "App": "Приложение" + }, + "UseVLC": "Предлагать открыть видео в VLC" }, "Size": "Размер", "SpecialThanks": "Отдельное спасибо", diff --git a/web/src/locales/ua/translation.json b/web/src/locales/ua/translation.json index c5941ce..45fc092 100644 --- a/web/src/locales/ua/translation.json +++ b/web/src/locales/ua/translation.json @@ -75,6 +75,16 @@ "Playlist": "Плейлист", "Preload": "Передзав.", "ProjectSource": "Сайт проекту", + "PWAGuide": { + "Header": "Встановити додаток", + "Description": "Встановіть програму на свій пристрій, щоб легко отримати до неї доступ у будь-який час. Немає магазину додатків. Немає завантаження.", + "VLC": "Кнопка VLC буде додана, щоб миттєво відкривати відео на телефоні", + "FirstStep": "Торкніться", + "SecondStep": { + "Select": "Виберіть", + "AddToHomeScreen":"Додати на головний екран" + } + }, "Releases": "Релізи TorrServer", "RemoveAll": "Видалити все", "RemoveViews": "Видалити перегляди", @@ -88,6 +98,7 @@ "SettingsDialog": { "AddRetrackers": "Додавати", "AdditionalSettings": "Додаткові налаштування", + "MobileAppSettings": "Установки моб. програми", "CacheBeforeReaderDesc": "з кешу буде збережено до поточного відтворюваного кадру", "CacheAfterReaderDesc": "з кешу буде завантажено після поточно відтвореного кадру", "CacheSize": "Размір кешу", @@ -125,8 +136,10 @@ "Tabs": { "Main": "Основні", "Additional": "Додаткові", - "AdditionalDisabled": "(включіть ПРО-режим)" - } + "AdditionalDisabled": "(включіть ПРО-режим)", + "App": "Додаток" + }, + "UseVLC": "Пропонувати відкрити відео у VLC" }, "Size": "Розмір", "SpecialThanks": "Окрема подяка", diff --git a/web/src/style/CustomMaterialUiStyles.js b/web/src/style/CustomMaterialUiStyles.js new file mode 100644 index 0000000..53ad621 --- /dev/null +++ b/web/src/style/CustomMaterialUiStyles.js @@ -0,0 +1,38 @@ +import { ListItem } from '@material-ui/core' +import Dialog from '@material-ui/core/Dialog' +import { pwaFooterHeight } from 'components/App/PWAFooter/style' +import styled, { css } from 'styled-components' +import { Header } from 'style/DialogStyles' +import { isStandaloneApp } from 'utils/Utils' + +import { standaloneMedia } from './standaloneMedia' + +export const StyledMenuButtonWrapper = styled(ListItem).attrs({ button: true })` + ${standaloneMedia(css` + width: 100%; + height: 60px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 10px; + `)} +` + +export const StyledDialog = styled(Dialog).attrs({ + ...(isStandaloneApp && { hideBackdrop: true, transitionDuration: 0 }), +})` + ${standaloneMedia(css` + margin-bottom: ${pwaFooterHeight}px; + + .MuiDialog-container .MuiPaper-root { + box-shadow: none; + } + `)} +` + +export const StyledHeader = styled(Header)` + ${standaloneMedia(css` + padding-top: 47px; + `)} +` diff --git a/web/src/style/GlobalStyle.js b/web/src/style/GlobalStyle.js index d3bc618..e4e1f89 100755 --- a/web/src/style/GlobalStyle.js +++ b/web/src/style/GlobalStyle.js @@ -1,4 +1,6 @@ -import { createGlobalStyle } from 'styled-components' +import { createGlobalStyle, css } from 'styled-components' + +import { standaloneMedia } from './standaloneMedia' export default createGlobalStyle` *, @@ -15,6 +17,12 @@ export default createGlobalStyle` -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: -0.1px; + -webkit-tap-highlight-color: transparent; + + + ${standaloneMedia(css` + height: 100vh; + `)} } button { diff --git a/web/src/style/materialUISetup.js b/web/src/style/materialUISetup.js index f30d2e4..875ee91 100644 --- a/web/src/style/materialUISetup.js +++ b/web/src/style/materialUISetup.js @@ -1,4 +1,4 @@ -import { createMuiTheme, useMediaQuery } from '@material-ui/core' +import { createTheme, useMediaQuery } from '@material-ui/core' import { useEffect, useMemo, useState } from 'react' import { mainColors } from './colors' @@ -7,7 +7,7 @@ export const THEME_MODES = { LIGHT: 'light', DARK: 'dark', AUTO: 'auto' } const typography = { fontFamily: 'Open Sans, sans-serif' } -export const darkTheme = createMuiTheme({ +export const darkTheme = createTheme({ typography, palette: { type: THEME_MODES.DARK, @@ -15,7 +15,7 @@ export const darkTheme = createMuiTheme({ secondary: { main: mainColors.dark.secondary }, }, }) -export const lightTheme = createMuiTheme({ +export const lightTheme = createTheme({ typography, palette: { type: THEME_MODES.LIGHT, @@ -45,7 +45,7 @@ export const useMaterialUITheme = () => { const muiTheme = useMemo( () => - createMuiTheme({ + createTheme({ typography, palette: { type: theme, diff --git a/web/src/style/standaloneMedia.js b/web/src/style/standaloneMedia.js new file mode 100644 index 0000000..df13ab8 --- /dev/null +++ b/web/src/style/standaloneMedia.js @@ -0,0 +1,7 @@ +import { css } from 'styled-components' + +export const standaloneMedia = styles => css` + @media screen and (display-mode: standalone) { + ${styles}; + } +` diff --git a/web/src/utils/Utils.js b/web/src/utils/Utils.js index 4e3dbb1..782a097 100644 --- a/web/src/utils/Utils.js +++ b/web/src/utils/Utils.js @@ -65,3 +65,5 @@ export const getTorrents = async () => { throw new Error(null) } } + +export const isStandaloneApp = window.matchMedia('screen and (display-mode: standalone)').matches diff --git a/web/src/utils/checkIsIOS.jsx b/web/src/utils/checkIsIOS.jsx new file mode 100644 index 0000000..27b55c0 --- /dev/null +++ b/web/src/utils/checkIsIOS.jsx @@ -0,0 +1,5 @@ +export default () => { + if (typeof window === `undefined` || typeof navigator === `undefined`) return false + + return /iPhone|iPad|iPod/i.test(navigator.userAgent || navigator.vendor) +} diff --git a/web/src/utils/useOnStandaloneAppOutsideClick.jsx b/web/src/utils/useOnStandaloneAppOutsideClick.jsx new file mode 100644 index 0000000..a59ac43 --- /dev/null +++ b/web/src/utils/useOnStandaloneAppOutsideClick.jsx @@ -0,0 +1,24 @@ +import { useEffect, useRef } from 'react' +import { isStandaloneApp } from 'utils/Utils' + +export default function useOnStandaloneAppOutsideClick(onClickOutside) { + const ref = useRef() + + useEffect(() => { + if (!isStandaloneApp) return + + const handleClickOutside = event => { + if (ref.current && !ref.current.contains(event.target)) { + onClickOutside && onClickOutside() + } + } + + document.addEventListener('click', handleClickOutside, true) + + return () => { + document.removeEventListener('click', handleClickOutside, true) + } + }) + + return ref +}