diff --git a/.github/workflows/deploying.yml b/.github/workflows/deploying.yml index cd8a6b57..92cdfbe8 100644 --- a/.github/workflows/deploying.yml +++ b/.github/workflows/deploying.yml @@ -109,7 +109,6 @@ jobs: prerelease: false - name: Upload release (PWA) - id: upload-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -120,7 +119,6 @@ jobs: asset_content_type: application/zip - name: Upload Release (Normal) - id: upload-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/index.html b/index.html index 4bfec5d1..3335369e 100644 --- a/index.html +++ b/index.html @@ -1,45 +1,41 @@ - - - - - - + - - - - - - + + + + + - - - + + + + + + - - + + + - - + + - - + + - movie-web + + - {{#if opensearchEnabled }} - - + movie-web - - - {{/if}} - - - -
- - - + {{/if}} + + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index b2bbd592..d2a5dbb2 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "immer": "^10.0.2", "iso-639-1": "^3.1.0", "lodash.isequal": "^4.5.0", + "nanoid": "^5.0.4", "node-forge": "^1.3.1", "ofetch": "^1.0.0", "react": "^17.0.2", @@ -98,6 +99,8 @@ "handlebars": "^4.7.7", "jsdom": "^21.1.0", "postcss": "^8.4.20", + "postcss-rtl": "^2.0.0", + "postcss-rtlcss": "^4.0.9", "prettier": "^2.5.1", "prettier-plugin-tailwindcss": "^0.1.7", "tailwind-scrollbar": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b318e25..c2e88592 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: lodash.isequal: specifier: ^4.5.0 version: 4.5.0 + nanoid: + specifier: ^5.0.4 + version: 5.0.4 node-forge: specifier: ^1.3.1 version: 1.3.1 @@ -223,6 +226,12 @@ devDependencies: postcss: specifier: '>=8.4.31' version: 8.4.31 + postcss-rtl: + specifier: ^2.0.0 + version: 2.0.0(postcss@8.4.31) + postcss-rtlcss: + specifier: ^4.0.9 + version: 4.0.9(postcss@8.4.31) prettier: specifier: ^2.5.1 version: 2.8.8 @@ -4747,6 +4756,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.4: + resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -5070,6 +5085,26 @@ packages: postcss-selector-parser: 6.0.13 dev: true + /postcss-rtl@2.0.0(postcss@8.4.31): + resolution: {integrity: sha512-vFu78CvaGY9BafWRHNgDm6OjUxzRCWWCrp+KtnyXdgwibLwb/j5ls8Z/ubvOsk9B/Q2NLwSPrXRARKMaa9RBmA==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.31 + rtlcss: 4.0.0 + dev: true + + /postcss-rtlcss@4.0.9(postcss@8.4.31): + resolution: {integrity: sha512-dCNKEf+FgTv+EA3XI8ysg2RnpS5s3/iZmU+9qpCNFxHU/BhK+4hz7jyCsCAfo0CLnDrMPtaQENhwb+EGm1wh7Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.31 + rtlcss: 4.1.1 + dev: true + /postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} @@ -5480,6 +5515,28 @@ packages: '@babel/runtime': 7.22.11 dev: false + /rtlcss@4.0.0: + resolution: {integrity: sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + escalade: 3.1.1 + picocolors: 1.0.0 + postcss: 8.4.31 + strip-json-comments: 3.1.1 + dev: true + + /rtlcss@4.1.1: + resolution: {integrity: sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + escalade: 3.1.1 + picocolors: 1.0.0 + postcss: 8.4.31 + strip-json-comments: 3.1.1 + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: diff --git a/src/assets/css/index.css b/src/assets/css/index.css index 74eb1270..08b24f68 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -222,3 +222,12 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower { outline: 2px solid theme('colors.themePreview.primary'); box-shadow: 0 0 10px theme('colors.themePreview.secondary'); } + +[dir="rtl"] .transform { + /* Invert horizontal X offset on transform (Tailwind RTL plugin does the rest) */ + transform: translate(calc(var(--tw-translate-x) * -1), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; +} +[dir="ltr"] .transform { + /* default - otherwise it overwrites*/ + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; +} diff --git a/src/assets/languages.ts b/src/assets/languages.ts index 13b06541..d6c47905 100644 --- a/src/assets/languages.ts +++ b/src/assets/languages.ts @@ -25,3 +25,6 @@ export const locales = { pirate, minion, }; +export type Locales = keyof typeof locales; + +export const rtlLocales: Locales[] = []; diff --git a/src/backend/helpers/report.ts b/src/backend/helpers/report.ts index f9ac89a6..b7c32645 100644 --- a/src/backend/helpers/report.ts +++ b/src/backend/helpers/report.ts @@ -1,4 +1,5 @@ import { ScrapeMedia } from "@movie-web/providers"; +import { nanoid } from "nanoid"; import { ofetch } from "ofetch"; import { useCallback } from "react"; @@ -8,6 +9,7 @@ import { PlayerMeta } from "@/stores/player/slices/source"; // for anybody who cares - these are anonymous metrics. // They are just used for figuring out if providers are broken or not const metricsEndpoint = "https://backend.movie-web.app/metrics/providers"; +const batchId = () => nanoid(32); export type ProviderMetric = { tmdbId: string; @@ -34,6 +36,7 @@ export async function reportProviders(items: ProviderMetric[]): Promise { method: "POST", body: { items, + batchId: batchId(), }, }); } diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 90e43d70..ec5e26cb 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { memo, useEffect, useRef } from "react"; export enum Icons { @@ -152,10 +153,18 @@ export const Icon = memo((props: IconProps) => { return ; } + const flipClass = + props.icon === Icons.ARROW_LEFT || + props.icon === Icons.ARROW_RIGHT || + props.icon === Icons.CHEVRON_LEFT || + props.icon === Icons.CHEVRON_RIGHT + ? "rtl:-scale-x-100" + : ""; + return ( ); }); diff --git a/src/components/overlays/positions/OverlayAnchorPosition.tsx b/src/components/overlays/positions/OverlayAnchorPosition.tsx index 028f721d..49cb7d69 100644 --- a/src/components/overlays/positions/OverlayAnchorPosition.tsx +++ b/src/components/overlays/positions/OverlayAnchorPosition.tsx @@ -64,7 +64,7 @@ export function OverlayAnchorPosition(props: AnchorPositionProps) { transform: `translateX(${left}px) translateY(${top}px)`, }} className={classNames([ - "pointer-events-auto z-10 inline-block origin-top-left touch-none", + "[&>*]:pointer-events-auto z-10 flex dir-neutral:items-start justify-start dir-neutral:origin-top-left touch-none", props.className, ])} > diff --git a/src/components/player/atoms/EpisodeTitle.tsx b/src/components/player/atoms/EpisodeTitle.tsx index b36909b1..1187d71a 100644 --- a/src/components/player/atoms/EpisodeTitle.tsx +++ b/src/components/player/atoms/EpisodeTitle.tsx @@ -9,8 +9,8 @@ export function EpisodeTitle() { if (meta?.type !== "show") return null; return ( -
- +
+ {t("media.episodeDisplay", { season: meta?.season?.number, episode: meta?.episode?.number, diff --git a/src/components/player/atoms/ProgressBar.tsx b/src/components/player/atoms/ProgressBar.tsx index c5deb7ca..56d8c472 100644 --- a/src/components/player/atoms/ProgressBar.tsx +++ b/src/components/player/atoms/ProgressBar.tsx @@ -119,7 +119,7 @@ export function ProgressBar() { }, [setDraggingTime, duration, dragPercentage]); return ( -
+
setHoveringAnyControls(true)} onMouseOut={() => setHoveringAnyControls(false)} - className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pb-3 mb-[env(safe-area-inset-bottom)] absolute bottom-0 w-full" + className="pointer-events-auto z-10 pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pb-3 mb-[env(safe-area-inset-bottom)] absolute bottom-0 w-full" > {props.children} diff --git a/src/components/player/internals/Button.tsx b/src/components/player/internals/Button.tsx index 16ddda0b..146e34fe 100644 --- a/src/components/player/internals/Button.tsx +++ b/src/components/player/internals/Button.tsx @@ -22,20 +22,14 @@ export const VideoPlayerButton = forwardRef< type="button" onClick={(e) => props.onClick?.(e.currentTarget as HTMLButtonElement)} className={classNames([ - "tabbable p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center", + "tabbable p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center gap-3", props.activeClass ?? "active:scale-110 active:bg-opacity-75 active:text-white", props.className ?? "", ])} > {props.icon && ( - + )} {props.children} diff --git a/src/components/utils/Lightbar.css b/src/components/utils/Lightbar.css index 06f7ffe8..d5d12310 100644 --- a/src/components/utils/Lightbar.css +++ b/src/components/utils/Lightbar.css @@ -6,7 +6,7 @@ user-select: none; } -.lightbar { +[dir] .lightbar { left: 50vw; transform: translateX(-50%); } @@ -16,13 +16,14 @@ width: 150vw; } - .lightbar { + [dir] .lightbar { left: -25vw; transform: initial; } + } -.lightbar { +[dir] .lightbar { display: flex; justify-content: center; align-items: center; @@ -31,7 +32,7 @@ animation: boot var(--d) var(--animation) forwards; } -.lightbar-visual { +[dir] .lightbar-visual { left: 0; --top: theme('colors.background.main'); --bottom: theme('colors.lightBar.light'); @@ -57,7 +58,6 @@ @keyframes boot { from { - opacity: 0.25; } @@ -74,4 +74,4 @@ 100% { transform: rotate(180deg) translateZ(0px) translateY(400px) scaleX(1); } -} +} \ No newline at end of file diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 48a25094..d5a180c3 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -88,7 +88,7 @@ export function PlayerPart(props: PlayerPartProps) { ) : null}
-
+
{status === playerStatus.PLAYING ? ( <> @@ -130,6 +130,7 @@ export function PlayerPart(props: PlayerPartProps) { + +
+ {/* functional routes */} diff --git a/src/stores/language/index.ts b/src/stores/language/index.tsx similarity index 70% rename from src/stores/language/index.ts rename to src/stores/language/index.tsx index 052127bc..ba33d59a 100644 --- a/src/stores/language/index.ts +++ b/src/stores/language/index.tsx @@ -1,8 +1,10 @@ import { useEffect } from "react"; +import { Helmet } from "react-helmet-async"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; +import { rtlLocales } from "@/assets/languages"; import i18n from "@/setup/i18n"; export interface LanguageStore { @@ -24,10 +26,18 @@ export const useLanguageStore = create( ) ); -export function useLanguageListener() { +export function LanguageProvider() { const language = useLanguageStore((s) => s.language); useEffect(() => { i18n.changeLanguage(language); }, [language]); + + const isRtl = rtlLocales.includes(language as any); + + return ( + + + + ); } diff --git a/tailwind.config.ts b/tailwind.config.ts index 5e9b9a84..913a59fd 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,5 +1,6 @@ import { allThemes, defaultTheme, safeThemeList } from "./themes"; import type { Config } from "tailwindcss"; +import plugin from "tailwindcss/plugin"; const themer = require("tailwindcss-themer"); @@ -41,6 +42,9 @@ const config: Config = { ...allThemes, ], }), + plugin(({ addVariant }) => { + addVariant("dir-neutral", "[dir] &"); + }), ], }; diff --git a/vite.config.ts b/vite.config.ts index 9a0018b0..8ebf8198 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,9 @@ import path from "path"; import { handlebars } from "./plugins/handlebars"; import { loadEnv } from "vite"; +import tailwind from "tailwindcss"; +import rtl from "postcss-rtlcss"; + export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); return { @@ -18,8 +21,8 @@ export default defineConfig(({ mode }) => { env.VITE_APP_DOMAIN + (env.VITE_NORMAL_ROUTER !== "true" ? "/#" : ""), domain: env.VITE_APP_DOMAIN, - env - } + env, + }, }), react({ babel: { @@ -31,24 +34,24 @@ export default defineConfig(({ mode }) => { modules: false, useBuiltIns: "entry", corejs: { - version: "3.29" - } - } - ] - ] - } + version: "3.29", + }, + }, + ], + ], + }, }), VitePWA({ disable: env.VITE_PWA_ENABLED !== "true", registerType: "autoUpdate", workbox: { maximumFileSizeToCacheInBytes: 4000000, // 4mb - globIgnores: ["**ping.txt**"] + globIgnores: ["**ping.txt**"], }, includeAssets: [ "favicon.ico", "apple-touch-icon.png", - "safari-pinned-tab.svg" + "safari-pinned-tab.svg", ], manifest: { name: "movie-web", @@ -63,48 +66,53 @@ export default defineConfig(({ mode }) => { src: "android-chrome-192x192.png", sizes: "192x192", type: "image/png", - purpose: "any" + purpose: "any", }, { src: "android-chrome-512x512.png", sizes: "512x512", type: "image/png", - purpose: "any" + purpose: "any", }, { src: "android-chrome-192x192.png", sizes: "192x192", type: "image/png", - purpose: "maskable" + purpose: "maskable", }, { src: "android-chrome-512x512.png", sizes: "512x512", type: "image/png", - purpose: "maskable" - } - ] - } + purpose: "maskable", + }, + ], + }, }), loadVersion(), checker({ overlay: { - position: "tr" + position: "tr", }, typescript: true, // check typescript build errors in dev server eslint: { // check lint errors in dev server lintCommand: "eslint --ext .tsx,.ts src", dev: { - logLevel: ["error"] - } - } - }) + logLevel: ["error"], + }, + }, + }), ], build: { sourcemap: true, }, + css: { + postcss: { + plugins: [tailwind(), rtl()], + }, + }, resolve: { alias: { @@ -112,12 +120,12 @@ export default defineConfig(({ mode }) => { "@sozialhelden/ietf-language-tags": path.resolve( __dirname, "./node_modules/@sozialhelden/ietf-language-tags/dist/cjs" - ) - } + ), + }, }, test: { - environment: "jsdom" - } + environment: "jsdom", + }, }; });