diff --git a/package.json b/package.json index f44fecd8..8e6c4f45 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@formkit/auto-animate": "^0.7.0", "@headlessui/react": "^1.5.0", - "@movie-web/providers": "^1.1.2", + "@movie-web/providers": "^1.1.3", "@noble/hashes": "^1.3.2", "@react-spring/web": "^9.7.1", "@scure/bip39": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d22330cf..50c3f495 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ dependencies: specifier: ^1.5.0 version: 1.7.17(react-dom@17.0.2)(react@17.0.2) '@movie-web/providers': - specifier: ^1.1.2 - version: 1.1.2 + specifier: ^1.1.3 + version: 1.1.3 '@noble/hashes': specifier: ^1.3.2 version: 1.3.2 @@ -1889,8 +1889,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@movie-web/providers@1.1.2: - resolution: {integrity: sha512-ZPSHBoz9WFLc6bWnRAXpefE+Vf8GNJ4xuWv5gu+uNg7dNBIMCnPqeuABlNIGxpEi68Go7zYlyx6nH/GQItgweA==} + /@movie-web/providers@1.1.3: + resolution: {integrity: sha512-6oxRqoZLVWQJHkJJaS1ZqDV7/LATYJ2EY0RKHhQUho3eFP5SpcdAvElllvvaRaomVFix8ftYYuy+NHWTbFox0g==} dependencies: cheerio: 1.0.0-rc.12 crypto-js: 4.2.0 diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 8d9b0b72..4d098125 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -2,6 +2,8 @@ "auth": { "deviceNameLabel": "Device name", "deviceNamePlaceholder": "Personal phone", + "hasAccount": "Already have an account? <0>Login here.", + "createAccount": "Dont have an account yet? <0>Create an account.", "register": { "information": { "title": "Account information", @@ -218,9 +220,18 @@ "stopEditing": "Stop editing" }, "titles": { - "morning": ["Morning title"], - "day": ["Day title"], - "night": ["Night title"] + "morning": { + "default": "What would you like to watch this morning?", + "extra": ["I hear Before Sunrise is good"] + }, + "day": { + "default": "What would you like to watch this afternoon?", + "extra": [] + }, + "night": { + "default": "What would you like to watch tonight?", + "extra": ["Tired? I hear The Excorcist is good."] + } }, "search": { "loading": "Loading...", @@ -369,15 +380,21 @@ } } }, - "faq": { - "title": "About us", + "about": { + "title": "About movie-web", + "description": "movie-web is a web application that searches the internet for streams. The team aims for a mostly minimalistic approach to consuming content.", + "faqTitle": "Common questions", "q1": { - "title": "1", - "body": "Body of 1" + "title": "Where does the content come from?", + "body": "movie-web does not host any content. When you click on something to watch, the internet is searched for the selected media (On the loading screen and in the 'video sources' tab you can see which source you're using). Media never gets uploaded by movie-web, everything is through this searching mechanism." }, - "how": { - "title": "1", - "body": "Body of 1" + "q2": { + "title": "Where can I request a show or movie?", + "body": "It's not possible to request a show or movie, movie-web does not manage any content. All content is viewed through sources on the internet." + }, + "q3": { + "title": "The search results display the show or movie, why can't I play it?", + "body": "Our search results are powered by The Movie Database (TMDB) and display regardless of whether our sources actually have the content." } }, "footer": { diff --git a/src/assets/locales/pirate.json b/src/assets/locales/pirate.json index bf69b1a2..b0048e82 100644 --- a/src/assets/locales/pirate.json +++ b/src/assets/locales/pirate.json @@ -53,17 +53,6 @@ "reloadPage": "Reload the page", "title": "That be an error, Captain" }, - "faq": { - "how": { - "body": "Body of 1", - "title": "1" - }, - "q1": { - "body": "Body of 1", - "title": "1" - }, - "title": "About us" - }, "footer": { "legal": { "disclaimer": "Disclaimer", @@ -104,17 +93,6 @@ "noResults": "We couldn't find anythin', arrr!", "placeholder": "What do ye want to watch?", "sectionTitle": "Searchin' results" - }, - "titles": { - "day": [ - "Day title" - ], - "morning": [ - "Morning title" - ], - "night": [ - "Night title" - ] } }, "media": { diff --git a/src/backend/accounts/progress.ts b/src/backend/accounts/progress.ts index a5ad5022..037e4c56 100644 --- a/src/backend/accounts/progress.ts +++ b/src/backend/accounts/progress.ts @@ -19,6 +19,7 @@ export interface ProgressInput { episodeId?: string; seasonNumber?: number; episodeNumber?: number; + updatedAt?: string; } export function progressUpdateItemToInput( @@ -60,6 +61,7 @@ export function progressMediaItemToInputs( seasonId: episode.seasonId, episodeNumber: episode.number, seasonNumber: item.seasons[episode.seasonId].number, + updatedAt: new Date(episode.updatedAt).toISOString(), })); } return [ @@ -67,6 +69,7 @@ export function progressMediaItemToInputs( duration: item.progress?.duration ?? 0, watched: item.progress?.watched ?? 0, tmdbId, + updatedAt: new Date(item.updatedAt).toISOString(), meta: { title: item.title ?? "", type: item.type ?? "", diff --git a/src/components/LinksDropdown.tsx b/src/components/LinksDropdown.tsx index fa4910ab..3063ddb4 100644 --- a/src/components/LinksDropdown.tsx +++ b/src/components/LinksDropdown.tsx @@ -139,12 +139,9 @@ export function LinksDropdown(props: { children: React.ReactNode }) { {t("navigation.menu.settings")} - + {t("navigation.menu.about")} - - {t("navigation.menu.support")} - {deviceName ? ( = { - search: ``, - bookmark: ``, - clock: ``, - eyeSlash: ``, - user: ``, + userGroup: ``, + couch: ``, + mobile: ``, + ticket: ``, + handcuffs: ``, }; export const UserIcon = memo((props: UserIconProps) => { const icon = iconList[props.icon]; - if (!icon) return ; + if (!icon) return ; return ( -
+
+
{props.children}
diff --git a/src/components/player/hooks/useCaptions.ts b/src/components/player/hooks/useCaptions.ts index 522b22be..ec99c4fb 100644 --- a/src/components/player/hooks/useCaptions.ts +++ b/src/components/player/hooks/useCaptions.ts @@ -7,6 +7,9 @@ import { useSubtitleStore } from "@/stores/subtitles"; export function useCaptions() { const setLanguage = useSubtitleStore((s) => s.setLanguage); const enabled = useSubtitleStore((s) => s.enabled); + const resetSubtitleSpecificSettings = useSubtitleStore( + (s) => s.resetSubtitleSpecificSettings + ); const setCaption = usePlayerStore((s) => s.setCaption); const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage); const captionList = usePlayerStore((s) => s.captionList); @@ -21,9 +24,10 @@ export function useCaptions() { srtData, url: caption.url, }); + resetSubtitleSpecificSettings(); setLanguage(language); }, - [setLanguage, captionList, setCaption] + [setLanguage, captionList, setCaption, resetSubtitleSpecificSettings] ); const disable = useCallback(async () => { diff --git a/src/components/text/Link.tsx b/src/components/text/Link.tsx new file mode 100644 index 00000000..0956b307 --- /dev/null +++ b/src/components/text/Link.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from "react"; +import { Link as LinkRouter } from "react-router-dom"; + +export function MwLink(props: { + children?: ReactNode; + to?: string; + url?: string; + onClick?: () => void; +}) { + const isExternal = !!props.url; + const isInternal = !!props.to; + const content = ( + + {props.children} + + ); + + if (isExternal) return {content}; + if (isInternal) return {content}; + return ( + props.onClick && props.onClick()}>{content} + ); +} diff --git a/src/components/utils/Ol.tsx b/src/components/utils/Ol.tsx index 3a3b5e70..a386f07e 100644 --- a/src/components/utils/Ol.tsx +++ b/src/components/utils/Ol.tsx @@ -8,7 +8,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
  • @@ -17,7 +17,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
    {i !== props.items.length - 1 ? (
    Math.floor(Math.random() * 10) === 0; + export function useRandomTranslation() { const { t } = useTranslation(); + const shouldJoke = useMemo(() => shouldGiveJokeTitle(), []); const seed = useMemo(() => Math.random(), []); const getRandomTranslation = useCallback( - (key: string) => { - const res = t(key, { returnObjects: true }); + (key: string): string => { + const defaultTitle = t(`${key}.default`) ?? ""; + if (!shouldJoke) return defaultTitle; - if (Array.isArray(res)) { - return res[Math.floor(seed * res.length)]; + const keys = t(`${key}.extra`, { returnObjects: true }); + if (Array.isArray(keys)) { + if (keys.length === 0) return defaultTitle; + return keys[Math.floor(seed * keys.length)]; } - return res; + return typeof keys === "string" ? keys : defaultTitle; }, - [t, seed] + [t, seed, shouldJoke] ); return { t: getRandomTranslation }; diff --git a/src/index.tsx b/src/index.tsx index 79daec72..ac372fee 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,4 @@ +import "@/setup/pwa"; import "core-js/stable"; import "./stores/__old/imports"; import "@/setup/ga"; @@ -10,7 +11,6 @@ import { HelmetProvider } from "react-helmet-async"; import { useTranslation } from "react-i18next"; import { BrowserRouter, HashRouter } from "react-router-dom"; import { useAsync } from "react-use"; -import { registerSW } from "virtual:pwa-register"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; @@ -40,9 +40,6 @@ if (key) { (window as any).initMW(conf().PROXY_URLS, key); } initializeChromecast(); -registerSW({ - immediate: true, -}); function LoadingScreen(props: { type: "user" | "lazy" }) { const mapping = { diff --git a/src/pages/About.tsx b/src/pages/About.tsx index fc916c4c..b40dd410 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -22,14 +22,22 @@ export function AboutPage() { - {t("faq.title")} + {t("about.title")} + {t("about.description")} + {t("about.faqTitle")}
      {t("faq.q1.body")}, + + {t("about.q1.body")} + , + + {t("about.q2.body")} + , + + {t("about.q3.body")} + , ]} /> - {t("faq.how.title")} - {t("faq.how.body")} ); diff --git a/src/pages/parts/auth/AccountCreatePart.tsx b/src/pages/parts/auth/AccountCreatePart.tsx index 7fa90ff7..1c4bd34d 100644 --- a/src/pages/parts/auth/AccountCreatePart.tsx +++ b/src/pages/parts/auth/AccountCreatePart.tsx @@ -2,8 +2,8 @@ import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; -import { ColorPicker } from "@/components/form/ColorPicker"; -import { IconPicker } from "@/components/form/IconPicker"; +import { ColorPicker, initialColor } from "@/components/form/ColorPicker"; +import { IconPicker, initialIcon } from "@/components/form/IconPicker"; import { Icon, Icons } from "@/components/Icon"; import { LargeCard, @@ -28,9 +28,9 @@ interface AccountCreatePartProps { export function AccountCreatePart(props: AccountCreatePartProps) { const [device, setDevice] = useState(""); - const [colorA, setColorA] = useState("#2E65CF"); - const [colorB, setColorB] = useState("#2E65CF"); - const [userIcon, setUserIcon] = useState(UserIcons.USER); + const [colorA, setColorA] = useState(initialColor); + const [colorB, setColorB] = useState(initialColor); + const [userIcon, setUserIcon] = useState(initialIcon); const { t } = useTranslation(); const [hasDeviceError, setHasDeviceError] = useState(false); diff --git a/src/pages/parts/auth/LoginFormPart.tsx b/src/pages/parts/auth/LoginFormPart.tsx index 82836e74..d87f6e14 100644 --- a/src/pages/parts/auth/LoginFormPart.tsx +++ b/src/pages/parts/auth/LoginFormPart.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useAsyncFn } from "react-use"; import { verifyValidMnemonic } from "@/backend/accounts/crypto"; @@ -10,6 +10,7 @@ import { LargeCardButtons, LargeCardText, } from "@/components/layout/LargeCard"; +import { MwLink } from "@/components/text/Link"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { useAuth } from "@/hooks/auth/useAuth"; import { useBookmarkStore } from "@/stores/bookmarks"; @@ -88,6 +89,11 @@ export function LoginFormPart(props: LoginFormPartProps) { {t("auth.login.submit")} +

      + + . + +

      ); } diff --git a/src/pages/parts/auth/TrustBackendPart.tsx b/src/pages/parts/auth/TrustBackendPart.tsx index 77a88409..9118eeae 100644 --- a/src/pages/parts/auth/TrustBackendPart.tsx +++ b/src/pages/parts/auth/TrustBackendPart.tsx @@ -12,6 +12,7 @@ import { LargeCardText, } from "@/components/layout/LargeCard"; import { Loading } from "@/components/layout/Loading"; +import { MwLink } from "@/components/text/Link"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { conf } from "@/setup/config"; @@ -60,16 +61,21 @@ export function TrustBackendPart(props: TrustBackendPartProps) { {cardContent}
    + - +

    + + . + +

    ); } diff --git a/src/pages/parts/home/HeroPart.tsx b/src/pages/parts/home/HeroPart.tsx index ea6e1112..e40a1aa0 100644 --- a/src/pages/parts/home/HeroPart.tsx +++ b/src/pages/parts/home/HeroPart.tsx @@ -1,4 +1,5 @@ import { useCallback, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import Sticky from "react-sticky-el"; import { SearchBarInput } from "@/components/form/SearchBar"; @@ -15,7 +16,8 @@ export interface HeroPartProps { } export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { - const { t } = useRandomTranslation(); + const { t: randomT } = useRandomTranslation(); + const { t } = useTranslation(); const [search, setSearch, setSearchUnFocus] = searchParams; const [, setShowBg] = useState(false); const bannerSize = useBannerSize(); @@ -32,7 +34,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { if (hour < 12) time = "morning"; else if (hour < 19) time = "day"; - const title = t(`home.titles.${time}`); + const title = randomT(`home.titles.${time}`); const inputRef = useRef(null); useSlashFocus(inputRef); @@ -41,7 +43,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
    - {title} + {title}
    diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index bafd64fd..14df6f31 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -102,9 +102,12 @@ export function PlayerPart(props: PlayerPartProps) { - ) : null} + {status === playerStatus.PLAYBACK_ERROR || + status === playerStatus.PLAYING ? ( + + ) : null}
  • diff --git a/src/setup/App.tsx b/src/setup/App.tsx index cebb464a..d532d824 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -92,7 +92,7 @@ function App() { - + {shouldHaveDmcaPage() ? ( diff --git a/src/setup/pwa.ts b/src/setup/pwa.ts new file mode 100644 index 00000000..e7147ea9 --- /dev/null +++ b/src/setup/pwa.ts @@ -0,0 +1,27 @@ +import { registerSW } from "virtual:pwa-register"; + +const intervalMS = 60 * 60 * 1000; + +registerSW({ + immediate: true, + onRegisteredSW(swUrl, r) { + if (!r) return; + setInterval(async () => { + if (!(!r.installing && navigator)) return; + + if ("connection" in navigator && !navigator.onLine) return; + + const resp = await fetch(swUrl, { + cache: "no-store", + headers: { + cache: "no-store", + "cache-control": "no-cache", + }, + }); + + if (resp?.status === 200) { + await r.update(); + } + }, intervalMS); + }, +}); diff --git a/src/stores/subtitles/index.ts b/src/stores/subtitles/index.ts index 1652ee93..1a461b3e 100644 --- a/src/stores/subtitles/index.ts +++ b/src/stores/subtitles/index.ts @@ -34,6 +34,7 @@ export interface SubtitleStore { setOverrideCasing(enabled: boolean): void; setDelay(delay: number): void; importSubtitleLanguage(lang: string | null): void; + resetSubtitleSpecificSettings(): void; } export const useSubtitleStore = create( @@ -51,6 +52,12 @@ export const useSubtitleStore = create( backgroundOpacity: 0.5, size: 1, }, + resetSubtitleSpecificSettings() { + set((s) => { + s.delay = 0; + s.overrideCasing = false; + }); + }, updateStyling(newStyling) { set((s) => { if (newStyling.backgroundOpacity !== undefined) diff --git a/src/utils/detectFeatures.ts b/src/utils/detectFeatures.ts index a82a3de3..58aa9893 100644 --- a/src/utils/detectFeatures.ts +++ b/src/utils/detectFeatures.ts @@ -1,4 +1,5 @@ import fscreen from "fscreen"; +import Hls from "hls.js"; export const isSafari = /^((?!chrome|android).)*safari/i.test( navigator.userAgent @@ -48,5 +49,6 @@ export function canWebkitPictureInPicture(): boolean { } export function canPlayHlsNatively(video: HTMLVideoElement): boolean { + if (Hls.isSupported()) return false; // no need to play natively return !!video.canPlayType("application/vnd.apple.mpegurl"); } diff --git a/themes/default.ts b/themes/default.ts index bc855bc4..4f773103 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -63,7 +63,7 @@ export const defaultTheme = { secondary: "#64647B", danger: "#F46E6E", link: "#A87FD1", - linkHover: "#A87FD1", + linkHover: "#ba8fe6", }, // search bar diff --git a/vite.config.ts b/vite.config.ts index 35e32efa..4fbb2776 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -42,6 +42,7 @@ export default defineConfig(({ mode }) => { disable: process.env.VITE_PWA_ENABLED !== "yes", registerType: "autoUpdate", workbox: { + maximumFileSizeToCacheInBytes: 4000000, // 4mb globIgnores: ["**ping.txt**"] }, includeAssets: [