mirror of
https://github.com/sussy-code/smov.git
synced 2025-01-01 16:37:39 +01:00
commit
c7f3f774bb
17 changed files with 1508 additions and 1358 deletions
48
.github/workflows/linting_annotate.yml
vendored
48
.github/workflows/linting_annotate.yml
vendored
|
@ -1,48 +0,0 @@
|
||||||
name: Annotate linting
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read # download artifact
|
|
||||||
checks: write # annotate
|
|
||||||
|
|
||||||
# this is done as a seperate workflow so
|
|
||||||
# the annotater has access to write to checks (to annotate)
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Linting and Testing"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
annotate:
|
|
||||||
name: Annotate linting
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download linting report
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
run_id: ${{github.event.workflow_run.id }},
|
|
||||||
});
|
|
||||||
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
|
||||||
return artifact.name == "eslint_report.json"
|
|
||||||
})[0];
|
|
||||||
const download = await github.rest.actions.downloadArtifact({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
artifact_id: matchArtifact.id,
|
|
||||||
archive_format: 'zip',
|
|
||||||
});
|
|
||||||
const fs = require('fs');
|
|
||||||
fs.writeFileSync('${{github.workspace}}/eslint_report.zip', Buffer.from(download.data));
|
|
||||||
|
|
||||||
- run: unzip eslint_report.zip
|
|
||||||
|
|
||||||
- name: Annotate linting
|
|
||||||
uses: ataylorme/eslint-annotate-action@v2
|
|
||||||
with:
|
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
report-json: "eslint_report.json"
|
|
11
.github/workflows/linting_testing.yml
vendored
11
.github/workflows/linting_testing.yml
vendored
|
@ -25,15 +25,8 @@ jobs:
|
||||||
- name: Install Yarn packages
|
- name: Install Yarn packages
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Run ESLint Report
|
- name: Run ESLint
|
||||||
run: yarn lint:report
|
run: yarn lint
|
||||||
# continue on error, so it still reports it in the next step
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: eslint_report.json
|
|
||||||
path: eslint_report.json
|
|
||||||
|
|
||||||
building:
|
building:
|
||||||
name: Build project
|
name: Build project
|
||||||
|
|
18
package.json
18
package.json
|
@ -8,6 +8,7 @@
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@react-spring/web": "^9.7.1",
|
"@react-spring/web": "^9.7.1",
|
||||||
"@use-gesture/react": "^10.2.24",
|
"@use-gesture/react": "^10.2.24",
|
||||||
|
"core-js": "^3.29.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dompurify": "^3.0.1",
|
"dompurify": "^3.0.1",
|
||||||
"fscreen": "^1.2.0",
|
"fscreen": "^1.2.0",
|
||||||
|
@ -44,9 +45,8 @@
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
"defaults",
|
||||||
"not dead",
|
"chrome > 90"
|
||||||
"not op_mini all"
|
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
|
@ -55,6 +55,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.21.3",
|
||||||
|
"@babel/preset-env": "^7.20.2",
|
||||||
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
"@tailwindcss/line-clamp": "^0.4.2",
|
"@tailwindcss/line-clamp": "^0.4.2",
|
||||||
"@types/chromecast-caf-sender": "^1.0.5",
|
"@types/chromecast-caf-sender": "^1.0.5",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
@ -65,13 +68,14 @@
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router": "^5.1.18",
|
"@types/react-helmet": "^6.1.6",
|
||||||
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-stickynode": "^4.0.0",
|
"@types/react-stickynode": "^4.0.0",
|
||||||
"@types/react-transition-group": "^4.4.5",
|
"@types/react-transition-group": "^4.4.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^5.13.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
|
@ -94,7 +98,7 @@
|
||||||
"vite-plugin-package-version": "^1.0.2",
|
"vite-plugin-package-version": "^1.0.2",
|
||||||
"vite-plugin-pwa": "^0.14.4",
|
"vite-plugin-pwa": "^0.14.4",
|
||||||
"vitest": "^0.28.5",
|
"vitest": "^0.28.5",
|
||||||
"workbox-window": "^6.5.4",
|
"workbox-build": "^6.5.4",
|
||||||
"@types/react-helmet": "^6.1.6"
|
"workbox-window": "^6.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export type JWMediaResult = {
|
||||||
title: string;
|
title: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
id: number;
|
id: number;
|
||||||
original_release_year: number;
|
original_release_year?: number;
|
||||||
jw_entity_id: string;
|
jw_entity_id: string;
|
||||||
object_type: JWContentTypes;
|
object_type: JWContentTypes;
|
||||||
seasons?: JWSeasonShort[];
|
seasons?: JWSeasonShort[];
|
||||||
|
@ -67,7 +67,7 @@ export function formatJWMeta(
|
||||||
return {
|
return {
|
||||||
title: media.title,
|
title: media.title,
|
||||||
id: media.id.toString(),
|
id: media.id.toString(),
|
||||||
year: media.original_release_year.toString(),
|
year: media.original_release_year?.toString(),
|
||||||
poster: media.poster
|
poster: media.poster
|
||||||
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
|
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type MWSeasonWithEpisodeMeta = {
|
||||||
type MWMediaMetaBase = {
|
type MWMediaMetaBase = {
|
||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: string;
|
||||||
year: string;
|
year?: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,9 @@ function MediaCardContent({
|
||||||
|
|
||||||
const canLink = linkable && !closable;
|
const canLink = linkable && !closable;
|
||||||
|
|
||||||
|
const dotListContent = [t(`media.${media.type}`)];
|
||||||
|
if (media.year) dotListContent.push(media.year);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
||||||
|
@ -115,10 +118,7 @@ function MediaCardContent({
|
||||||
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
|
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
|
||||||
<span>{media.title}</span>
|
<span>{media.title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<DotList
|
<DotList className="text-xs" content={dotListContent} />
|
||||||
className="text-xs"
|
|
||||||
content={[t(`media.${media.type}`), media.year]}
|
|
||||||
/>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { ReactNode, Suspense } from "react";
|
import "core-js/stable";
|
||||||
|
import React, { Suspense } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||||
|
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
||||||
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
@ -21,9 +23,7 @@ if (key) {
|
||||||
}
|
}
|
||||||
initializeChromecast();
|
initializeChromecast();
|
||||||
registerSW({
|
registerSW({
|
||||||
onNeedRefresh() {
|
immediate: true,
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const LazyLoadedApp = React.lazy(async () => {
|
const LazyLoadedApp = React.lazy(async () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
|
export const APP_VERSION = import.meta.env.PACKAGE_VERSION;
|
||||||
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
|
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
|
||||||
export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
|
export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
|
||||||
export const APP_VERSION = "3.0.6";
|
|
||||||
export const GA_ID = "G-44YVXRL61C";
|
export const GA_ID = "G-44YVXRL61C";
|
||||||
|
|
|
@ -94,7 +94,6 @@ input[type=range].styled-slider::-webkit-slider-runnable-track {
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: var(--slider-border-radius);
|
border-radius: var(--slider-border-radius);
|
||||||
margin-bottom: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range].styled-slider::-webkit-slider-thumb:hover {
|
input[type=range].styled-slider::-webkit-slider-thumb:hover {
|
||||||
|
|
|
@ -99,7 +99,7 @@ function buildStorageObject<T>(store: InternalStoreData): StoreRet<T> {
|
||||||
localStorage.setItem(key, JSON.stringify(withVersion));
|
localStorage.setItem(key, JSON.stringify(withVersion));
|
||||||
|
|
||||||
if (!storeCallbacks[key]) storeCallbacks[key] = [];
|
if (!storeCallbacks[key]) storeCallbacks[key] = [];
|
||||||
storeCallbacks[key].forEach((v) => v(structuredClone(data)));
|
storeCallbacks[key].forEach((v) => v(window.structuredClone(data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
import { QualityDisplayAction } from "./QualityDisplayAction";
|
import { QualityDisplayAction } from "./QualityDisplayAction";
|
||||||
|
|
|
@ -41,7 +41,7 @@ function VideoElement(props: Props) {
|
||||||
autoPlay={props.autoPlay}
|
autoPlay={props.autoPlay}
|
||||||
muted={mediaPlaying.volume === 0}
|
muted={mediaPlaying.volume === 0}
|
||||||
playsInline
|
playsInline
|
||||||
className="h-full w-full"
|
className="z-0 h-full w-full"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { Component, ReactNode } from "react";
|
import { Component } from "react";
|
||||||
|
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
|
|
@ -43,6 +43,8 @@ export function ScrollToActive(props: ScrollToActiveProps) {
|
||||||
const ref = createRef<HTMLDivElement>();
|
const ref = createRef<HTMLDivElement>();
|
||||||
const inited = useRef<boolean>(false);
|
const inited = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const SAFE_OFFSET = 30;
|
||||||
|
|
||||||
// Scroll to "active" child on first load (AKA mount except React dumb)
|
// Scroll to "active" child on first load (AKA mount except React dumb)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inited.current) return;
|
if (inited.current) return;
|
||||||
|
@ -61,27 +63,31 @@ export function ScrollToActive(props: ScrollToActiveProps) {
|
||||||
wrapper?.querySelector(".active");
|
wrapper?.querySelector(".active");
|
||||||
|
|
||||||
if (wrapper && active) {
|
if (wrapper && active) {
|
||||||
let activeYPositionCentered = 0;
|
let wrapperHeight = 0;
|
||||||
const setActiveYPositionCentered = () => {
|
let activePos = 0;
|
||||||
activeYPositionCentered =
|
let activeHeight = 0;
|
||||||
active.getBoundingClientRect().top -
|
let wrapperScroll = 0;
|
||||||
wrapper.getBoundingClientRect().top +
|
|
||||||
active.offsetHeight / 2;
|
const getCoords = () => {
|
||||||
|
const activeRect = active.getBoundingClientRect();
|
||||||
|
const wrapperRect = wrapper.getBoundingClientRect();
|
||||||
|
wrapperHeight = wrapperRect.height;
|
||||||
|
activeHeight = activeRect.height;
|
||||||
|
activePos = activeRect.top - wrapperRect.top + wrapper.scrollTop;
|
||||||
|
wrapperScroll = wrapper.scrollTop;
|
||||||
};
|
};
|
||||||
setActiveYPositionCentered();
|
getCoords();
|
||||||
|
|
||||||
if (activeYPositionCentered >= wrapper.offsetHeight / 2) {
|
const isVisible =
|
||||||
// Check if the active element is below the vertical center line, then scroll it into center
|
activePos + activeHeight <
|
||||||
|
wrapperScroll + wrapperHeight - SAFE_OFFSET ||
|
||||||
|
activePos > wrapperScroll + SAFE_OFFSET;
|
||||||
|
if (isVisible) {
|
||||||
|
const activeMiddlePos = activePos + activeHeight / 2; // pos of middle of active element
|
||||||
|
const viewMiddle = wrapperHeight / 2; // half of the available height
|
||||||
|
const pos = activeMiddlePos - viewMiddle;
|
||||||
wrapper.scrollTo({
|
wrapper.scrollTo({
|
||||||
top: activeYPositionCentered - wrapper.offsetHeight / 2,
|
top: pos,
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveYPositionCentered();
|
|
||||||
if (activeYPositionCentered > wrapper.offsetHeight / 2) {
|
|
||||||
// If the element is over the vertical center line, scroll to the end
|
|
||||||
wrapper.scrollTo({
|
|
||||||
top: wrapper.scrollHeight,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react";
|
||||||
import loadVersion from "vite-plugin-package-version";
|
import loadVersion from "vite-plugin-package-version";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import checker from "vite-plugin-checker";
|
import checker from "vite-plugin-checker";
|
||||||
|
@ -7,10 +7,25 @@ import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react({
|
||||||
|
babel: {
|
||||||
|
presets: [
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
modules: false,
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
corejs: {
|
||||||
|
version: "3.29",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
injectRegister: "inline",
|
|
||||||
workbox: {
|
workbox: {
|
||||||
globIgnores: ["**ping.txt**"],
|
globIgnores: ["**ping.txt**"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue