feat: implement intro submission feature and update to beta.14

This commit is contained in:
paregi12 2026-01-31 14:08:39 +05:30
parent 28216b475f
commit 8d9fed3f7f
15 changed files with 828 additions and 121 deletions

View file

@ -0,0 +1,10 @@
# Nuvio Alpha Build 2
This is the second alpha release of Nuvio!
## What's New
- **Intro Submission:** You can now submit intro timestamps directly to IntroDB!
- **Bug Fixes:** Various improvements and stability fixes.
## Installation
Download the attached APK and install it on your Android device.

132
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "nuvio", "name": "nuvio",
"version": "0.6.0-beta.6", "version": "0.6.0-beta.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nuvio", "name": "nuvio",
"version": "0.6.0-beta.6", "version": "0.6.0-beta.16",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@adrianso/react-native-device-brightness": "^1.2.7", "@adrianso/react-native-device-brightness": "^1.2.7",
@ -19,7 +19,7 @@
"@gorhom/bottom-sheet": "^5.2.6", "@gorhom/bottom-sheet": "^5.2.6",
"@kesha-antonov/react-native-background-downloader": "^4.4.5", "@kesha-antonov/react-native-background-downloader": "^4.4.5",
"@legendapp/list": "^2.0.13", "@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.17.7", "@lottiefiles/dotlottie-react": "^0.13.5",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1", "@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^5.1.1", "@react-native-community/slider": "^5.1.1",
@ -1540,6 +1540,7 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1798,6 +1799,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@expo/cli/node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@expo/code-signing-certificates": { "node_modules/@expo/code-signing-certificates": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz",
@ -2088,6 +2098,7 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz", "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz",
"integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==", "integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"anser": "^1.4.9", "anser": "^1.4.9",
"pretty-format": "^29.7.0", "pretty-format": "^29.7.0",
@ -2576,6 +2587,7 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jimp/core": "^0.22.12" "@jimp/core": "^0.22.12"
} }
@ -2757,21 +2769,22 @@
} }
}, },
"node_modules/@lottiefiles/dotlottie-react": { "node_modules/@lottiefiles/dotlottie-react": {
"version": "0.17.10", "version": "0.13.5",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.10.tgz", "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.5.tgz",
"integrity": "sha512-ikrN05/q0/KjqIU+n48uNwmE7DeZIC9y3Nd19httcKqe273zoOeNYycEaQzLSdcpEGnWLmHaZpgtoo07aQZAXg==", "integrity": "sha512-4U5okwjRqDPkjB572hfZtLXJ/LGfCo6vDwUB2KIPEUoSgqbIlw+UrbnaqVp3GS+dRvhMD27F2JObpHpYRlpF0Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lottiefiles/dotlottie-web": "0.58.1" "@lottiefiles/dotlottie-web": "0.44.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^17 || ^18 || ^19" "react": "^17 || ^18 || ^19"
} }
}, },
"node_modules/@lottiefiles/dotlottie-web": { "node_modules/@lottiefiles/dotlottie-web": {
"version": "0.58.1", "version": "0.44.0",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.58.1.tgz", "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.44.0.tgz",
"integrity": "sha512-YC4pmScrV0R3rd11gU5xHrjeNczlCic69zlnMH/buDIzYxIbpR88oPUhGtKgu5ln7EJchoLpeRJbA3uLCzSeTA==", "integrity": "sha512-IUWKVciDJI/BMWDWnh7j0Ngd0N8q9ySRAwm84aDqIE07qpmdZ7x1rkIpBaU1yHSNqNYHeh1Rxsl+LC3CY4f0KA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@posthog/core": { "node_modules/@posthog/core": {
@ -3116,7 +3129,7 @@
"version": "0.72.8", "version": "0.72.8",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"invariant": "^2.2.4", "invariant": "^2.2.4",
@ -3237,6 +3250,7 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==", "integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@react-navigation/core": "^7.13.6", "@react-navigation/core": "^7.13.6",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
@ -3878,6 +3892,7 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0", "@svgr/babel-preset": "8.1.0",
@ -4098,6 +4113,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@ -4107,8 +4123,9 @@
"version": "0.72.8", "version": "0.72.8",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@react-native/virtualized-lists": "^0.72.4", "@react-native/virtualized-lists": "^0.72.4",
"@types/react": "*" "@types/react": "*"
@ -4644,6 +4661,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.4", "form-data": "^4.0.4",
@ -5047,6 +5065,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -6276,6 +6295,7 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz",
"integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==", "integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.19", "@expo/cli": "54.0.19",
@ -6479,6 +6499,7 @@
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz", "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==", "integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ua-parser-js": "^0.7.33" "ua-parser-js": "^0.7.33"
}, },
@ -6506,6 +6527,7 @@
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
"integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==", "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"expo": "*", "expo": "*",
"react-native": "*" "react-native": "*"
@ -6516,6 +6538,7 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz",
"integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==", "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fontfaceobserver": "^2.1.0" "fontfaceobserver": "^2.1.0"
}, },
@ -6611,6 +6634,7 @@
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz", "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz",
"integrity": "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==", "integrity": "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"rtl-detect": "^1.0.2" "rtl-detect": "^1.0.2"
}, },
@ -7670,6 +7694,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4" "@babel/runtime": "^7.28.4"
}, },
@ -8286,6 +8311,20 @@
"xml-name-validator": ">= 2.0.1 < 3.0.0" "xml-name-validator": ">= 2.0.1 < 3.0.0"
} }
}, },
"node_modules/jsdom/node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@ -10562,6 +10601,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -10602,6 +10642,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -10659,6 +10700,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz",
"integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==", "integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/create-cache-key-function": "^29.7.0", "@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.4", "@react-native/assets-registry": "0.81.4",
@ -10747,6 +10789,7 @@
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-1.1.0.tgz",
"integrity": "sha512-Uu1gvM3i1Hb4DjVvR/38J1QVQEs0RkPc7K6yon99HgvRWWOyLs7kjPDhUswtb8ije4pKW712skIXWJ0lgKzbyQ==", "integrity": "sha512-Uu1gvM3i1Hb4DjVvR/38J1QVQEs0RkPc7K6yon99HgvRWWOyLs7kjPDhUswtb8ije4pKW712skIXWJ0lgKzbyQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-freeze": "^1.0.0", "react-freeze": "^1.0.0",
"sf-symbols-typescript": "^2.0.0", "sf-symbols-typescript": "^2.0.0",
@ -10777,6 +10820,7 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.1.tgz", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.1.tgz",
"integrity": "sha512-du3qmv0e3Sm7qsd9SfmHps+AggLiylcBBQ8ztz7WUtd8ZjKs5V3kekAbi9R2W9bRLSg47Ntp4GGMYZOhikQdZA==", "integrity": "sha512-du3qmv0e3Sm7qsd9SfmHps+AggLiylcBBQ8ztz7WUtd8ZjKs5V3kekAbi9R2W9bRLSg47Ntp4GGMYZOhikQdZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@egjs/hammerjs": "^2.0.17", "@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
@ -10875,6 +10919,7 @@
"integrity": "sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==", "integrity": "sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
@ -10940,6 +10985,7 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.0.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.0.tgz",
"integrity": "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg==", "integrity": "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-native-is-edge-to-edge": "1.2.1", "react-native-is-edge-to-edge": "1.2.1",
"semver": "7.7.3" "semver": "7.7.3"
@ -10979,6 +11025,7 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
@ -10989,6 +11036,7 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz",
"integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==", "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-freeze": "^1.0.0", "react-freeze": "^1.0.0",
"warn-once": "^0.1.0" "warn-once": "^0.1.0"
@ -11003,6 +11051,7 @@
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz",
"integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==", "integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"css-select": "^5.1.0", "css-select": "^5.1.0",
"css-tree": "^1.1.3", "css-tree": "^1.1.3",
@ -11231,6 +11280,7 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.6", "@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1", "@react-native/normalize-colors": "^0.74.1",
@ -11487,6 +11537,7 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -11690,6 +11741,20 @@
"node": ">= 0.12" "node": ">= 0.12"
} }
}, },
"node_modules/request/node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -12939,6 +13004,24 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/tldts": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.19"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
"license": "MIT"
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@ -12994,17 +13077,16 @@
} }
}, },
"node_modules/tough-cookie": { "node_modules/tough-cookie": {
"version": "2.5.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true, "peer": true,
"dependencies": { "dependencies": {
"psl": "^1.1.28", "tldts": "^7.0.5"
"punycode": "^2.1.1"
}, },
"engines": { "engines": {
"node": ">=0.8" "node": ">=16"
} }
}, },
"node_modules/tr46": { "node_modules/tr46": {
@ -13081,8 +13163,9 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -13123,15 +13206,6 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",

View file

@ -1,11 +1,12 @@
{ {
"name": "nuvio", "name": "nuvio",
"version": "0.6.0-beta.6", "version": "0.6.0-beta.16",
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"build": "export NODE_ENV=production && cd android && ./gradlew assembleRelease",
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
@ -19,7 +20,7 @@
"@gorhom/bottom-sheet": "^5.2.6", "@gorhom/bottom-sheet": "^5.2.6",
"@kesha-antonov/react-native-background-downloader": "^4.4.5", "@kesha-antonov/react-native-background-downloader": "^4.4.5",
"@legendapp/list": "^2.0.13", "@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.17.7", "@lottiefiles/dotlottie-react": "^0.13.5",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1", "@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^5.1.1", "@react-native-community/slider": "^5.1.1",
@ -110,5 +111,8 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"xcode": "^3.0.1" "xcode": "^3.0.1"
}, },
"overrides": {
"@types/react": "~18.3.12"
},
"private": true "private": true
} }

View file

@ -0,0 +1,38 @@
diff --git a/node_modules/react-native-bottom-tabs/app.plugin.js b/node_modules/react-native-bottom-tabs/app.plugin.js
index 87edf84..711d0c0 100644
--- a/node_modules/react-native-bottom-tabs/app.plugin.js
+++ b/node_modules/react-native-bottom-tabs/app.plugin.js
@@ -1 +1,31 @@
-module.exports = require('./lib/module/expo');
+"use strict";
+
+const { createRunOncePlugin, withAndroidStyles } = require("@expo/config-plugins");
+
+const MATERIAL3_THEME_DYANMIC = "Theme.Material3.DynamicColors.DayNight.NoActionBar";
+const MATERIAL3_THEME = "Theme.Material3.DayNight.NoActionBar";
+const MATERIAL2_THEME = "Theme.MaterialComponents.DayNight.NoActionBar";
+const MATERIAL3_EXPRESSIVE_THEME = "Theme.Material3Expressive.DayNight.NoActionBar";
+
+const withMaterial3Theme = (config, options) => {
+ const theme = options?.theme;
+ return withAndroidStyles(config, stylesConfig => {
+ stylesConfig.modResults.resources.style = stylesConfig.modResults.resources.style?.map(style => {
+ if (style.$.name === "AppTheme") {
+ if (theme === "material3-dynamic") {
+ style.$.parent = MATERIAL3_THEME_DYANMIC;
+ } else if (theme === "material2") {
+ style.$.parent = MATERIAL2_THEME;
+ } else if (theme === "material3-expressive") {
+ style.$.parent = MATERIAL3_EXPRESSIVE_THEME;
+ } else {
+ style.$.parent = MATERIAL3_THEME;
+ }
+ }
+ return style;
+ });
+ return stylesConfig;
+ });
+};
+
+module.exports = createRunOncePlugin(withMaterial3Theme, "react-native-bottom-tabs");
\ No newline at end of file

View file

@ -35,6 +35,7 @@ import { AudioTrackModal } from './modals/AudioTrackModal';
import { SubtitleModals } from './modals/SubtitleModals'; import { SubtitleModals } from './modals/SubtitleModals';
import { SubtitleSyncModal } from './modals/SubtitleSyncModal'; import { SubtitleSyncModal } from './modals/SubtitleSyncModal';
import SpeedModal from './modals/SpeedModal'; import SpeedModal from './modals/SpeedModal';
import { SubmitIntroModal } from './modals/SubmitIntroModal';
import { SourcesModal } from './modals/SourcesModal'; import { SourcesModal } from './modals/SourcesModal';
import { EpisodesModal } from './modals/EpisodesModal'; import { EpisodesModal } from './modals/EpisodesModal';
import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal'; import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal';
@ -917,6 +918,7 @@ const AndroidVideoPlayer: React.FC = () => {
setShowAudioModal={modals.setShowAudioModal} setShowAudioModal={modals.setShowAudioModal}
setShowSubtitleModal={modals.setShowSubtitleModal} setShowSubtitleModal={modals.setShowSubtitleModal}
setShowSpeedModal={modals.setShowSpeedModal} setShowSpeedModal={modals.setShowSpeedModal}
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
isSubtitleModalOpen={modals.showSubtitleModal} isSubtitleModalOpen={modals.showSubtitleModal}
setShowSourcesModal={modals.setShowSourcesModal} setShowSourcesModal={modals.setShowSourcesModal}
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined} setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
@ -932,6 +934,7 @@ const AndroidVideoPlayer: React.FC = () => {
onSwitchToMPV={handleManualSwitchToMPV} onSwitchToMPV={handleManualSwitchToMPV}
useExoPlayer={useExoPlayer} useExoPlayer={useExoPlayer}
isBuffering={playerState.isBuffering} isBuffering={playerState.isBuffering}
imdbId={imdbId}
/> />
<SpeedActivatedOverlay <SpeedActivatedOverlay
@ -1106,6 +1109,15 @@ const AndroidVideoPlayer: React.FC = () => {
setHoldToSpeedValue={speedControl.setHoldToSpeedValue} setHoldToSpeedValue={speedControl.setHoldToSpeedValue}
/> />
<SubmitIntroModal
visible={modals.showSubmitIntroModal}
onClose={() => modals.setShowSubmitIntroModal(false)}
currentTime={playerState.currentTime}
imdbId={imdbId}
season={season}
episode={episode}
/>
<EpisodesModal <EpisodesModal
showEpisodesModal={modals.showEpisodesModal} showEpisodesModal={modals.showEpisodesModal}
setShowEpisodesModal={modals.setShowEpisodesModal} setShowEpisodesModal={modals.setShowEpisodesModal}

View file

@ -10,6 +10,7 @@ import UpNextButton from './common/UpNextButton';
import { PlayerControls } from './controls/PlayerControls'; import { PlayerControls } from './controls/PlayerControls';
import AudioTrackModal from './modals/AudioTrackModal'; import AudioTrackModal from './modals/AudioTrackModal';
import SpeedModal from './modals/SpeedModal'; import SpeedModal from './modals/SpeedModal';
import { SubmitIntroModal } from './modals/SubmitIntroModal';
import SubtitleModals from './modals/SubtitleModals'; import SubtitleModals from './modals/SubtitleModals';
import { SubtitleSyncModal } from './modals/SubtitleSyncModal'; import { SubtitleSyncModal } from './modals/SubtitleSyncModal';
import SourcesModal from './modals/SourcesModal'; import SourcesModal from './modals/SourcesModal';
@ -868,6 +869,7 @@ const KSPlayerCore: React.FC = () => {
setShowAudioModal={modals.setShowAudioModal} setShowAudioModal={modals.setShowAudioModal}
setShowSubtitleModal={modals.setShowSubtitleModal} setShowSubtitleModal={modals.setShowSubtitleModal}
setShowSpeedModal={modals.setShowSpeedModal} setShowSpeedModal={modals.setShowSpeedModal}
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
isSubtitleModalOpen={modals.showSubtitleModal} isSubtitleModalOpen={modals.showSubtitleModal}
setShowSourcesModal={modals.setShowSourcesModal} setShowSourcesModal={modals.setShowSourcesModal}
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined} setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
@ -881,6 +883,7 @@ const KSPlayerCore: React.FC = () => {
allowsAirPlay={allowsAirPlay} allowsAirPlay={allowsAirPlay}
onAirPlayPress={() => ksPlayerRef.current?.showAirPlayPicker()} onAirPlayPress={() => ksPlayerRef.current?.showAirPlayPicker()}
isBuffering={isBuffering} isBuffering={isBuffering}
imdbId={imdbId}
/> />
</View> </View>
)} )}
@ -998,6 +1001,15 @@ const KSPlayerCore: React.FC = () => {
setHoldToSpeedValue={speedControl.setHoldToSpeedValue} setHoldToSpeedValue={speedControl.setHoldToSpeedValue}
/> />
<SubmitIntroModal
visible={modals.showSubmitIntroModal}
onClose={() => modals.setShowSubmitIntroModal(false)}
currentTime={currentTime}
imdbId={imdbId}
season={season}
episode={episode}
/>
<SubtitleModals <SubtitleModals
showSubtitleModal={modals.showSubtitleModal} showSubtitleModal={modals.showSubtitleModal}
setShowSubtitleModal={modals.setShowSubtitleModal} setShowSubtitleModal={modals.setShowSubtitleModal}

View file

@ -8,6 +8,10 @@ import { useTranslation } from 'react-i18next';
import { styles } from '../utils/playerStyles'; // Updated styles import { styles } from '../utils/playerStyles'; // Updated styles
import { getTrackDisplayName } from '../utils/playerUtils'; import { getTrackDisplayName } from '../utils/playerUtils';
import { useTheme } from '../../../contexts/ThemeContext'; import { useTheme } from '../../../contexts/ThemeContext';
import { useSettings } from '../../../hooks/useSettings';
import { introService } from '../../../services/introService';
import { toastService } from '../../../services/toastService';
interface PlayerControlsProps { interface PlayerControlsProps {
showControls: boolean; showControls: boolean;
@ -37,6 +41,7 @@ interface PlayerControlsProps {
setShowAudioModal: (show: boolean) => void; setShowAudioModal: (show: boolean) => void;
setShowSubtitleModal: (show: boolean) => void; setShowSubtitleModal: (show: boolean) => void;
setShowSpeedModal: (show: boolean) => void; setShowSpeedModal: (show: boolean) => void;
setShowSubmitIntroModal: (show: boolean) => void;
isSubtitleModalOpen?: boolean; isSubtitleModalOpen?: boolean;
setShowSourcesModal?: (show: boolean) => void; setShowSourcesModal?: (show: boolean) => void;
setShowEpisodesModal?: (show: boolean) => void; setShowEpisodesModal?: (show: boolean) => void;
@ -55,6 +60,7 @@ interface PlayerControlsProps {
onSwitchToMPV?: () => void; onSwitchToMPV?: () => void;
useExoPlayer?: boolean; useExoPlayer?: boolean;
isBuffering?: boolean; isBuffering?: boolean;
imdbId?: string;
} }
export const PlayerControls: React.FC<PlayerControlsProps> = ({ export const PlayerControls: React.FC<PlayerControlsProps> = ({
@ -85,6 +91,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
setShowAudioModal, setShowAudioModal,
setShowSubtitleModal, setShowSubtitleModal,
setShowSpeedModal, setShowSpeedModal,
setShowSubmitIntroModal,
isSubtitleModalOpen, isSubtitleModalOpen,
setShowSourcesModal, setShowSourcesModal,
setShowEpisodesModal, setShowEpisodesModal,
@ -100,10 +107,17 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
onSwitchToMPV, onSwitchToMPV,
useExoPlayer, useExoPlayer,
isBuffering = false, isBuffering = false,
imdbId,
}) => { }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
// --- Intro Submission Logic ---
const handleIntroPress = () => {
setShowSubmitIntroModal(true);
};
/* Responsive Spacing */ /* Responsive Spacing */
const screenWidth = Dimensions.get('window').width; const screenWidth = Dimensions.get('window').width;
@ -619,6 +633,20 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
/> />
</TouchableOpacity> </TouchableOpacity>
{/* Submit Intro Button */}
{season !== undefined && episode !== undefined && settings.introSubmitEnabled && settings.introDbApiKey && (
<TouchableOpacity
style={styles.iconButton}
onPress={handleIntroPress}
>
<Ionicons
name="flag-outline"
size={24}
color="white"
/>
</TouchableOpacity>
)}
{/* Right Side: Episodes Button */} {/* Right Side: Episodes Button */}
{setShowEpisodesModal && ( {setShowEpisodesModal && (
<TouchableOpacity <TouchableOpacity

View file

@ -15,6 +15,7 @@ export const usePlayerModals = () => {
const [showErrorModal, setShowErrorModal] = useState(false); const [showErrorModal, setShowErrorModal] = useState(false);
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false);
const [showCastDetails, setShowCastDetails] = useState(false); const [showCastDetails, setShowCastDetails] = useState(false);
const [showSubmitIntroModal, setShowSubmitIntroModal] = useState(false);
// Some modals have associated data // Some modals have associated data
const [selectedEpisodeForStreams, setSelectedEpisodeForStreams] = useState<Episode | null>(null); const [selectedEpisodeForStreams, setSelectedEpisodeForStreams] = useState<Episode | null>(null);
@ -31,6 +32,7 @@ export const usePlayerModals = () => {
showErrorModal, setShowErrorModal, showErrorModal, setShowErrorModal,
showSubtitleLanguageModal, setShowSubtitleLanguageModal, showSubtitleLanguageModal, setShowSubtitleLanguageModal,
showCastDetails, setShowCastDetails, showCastDetails, setShowCastDetails,
showSubmitIntroModal, setShowSubmitIntroModal,
selectedEpisodeForStreams, setSelectedEpisodeForStreams, selectedEpisodeForStreams, setSelectedEpisodeForStreams,
errorDetails, setErrorDetails, errorDetails, setErrorDetails,
selectedCastMember, setSelectedCastMember selectedCastMember, setSelectedCastMember

View file

@ -0,0 +1,347 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet, TextInput, ActivityIndicator } from 'react-native';
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import Animated, {
FadeIn,
FadeOut,
SlideInDown,
SlideOutDown,
} from 'react-native-reanimated';
import { useSettings } from '../../../hooks/useSettings';
import { introService } from '../../../services/introService';
import { toastService } from '../../../services/toastService';
interface SubmitIntroModalProps {
visible: boolean;
onClose: () => void;
currentTime: number;
imdbId?: string;
season?: number;
episode?: number;
}
/**
* Parses time string (MM:SS or SS) to seconds
*/
const parseTimeToSeconds = (input: string): number | null => {
if (!input) return null;
// Format: MM:SS
if (input.includes(':')) {
const parts = input.split(':');
if (parts.length !== 2) return null;
const mins = parseInt(parts[0], 10);
const secs = parseInt(parts[1], 10);
if (isNaN(mins) || isParseSecs(secs)) return null;
return mins * 60 + secs;
}
// Format: Seconds only
const secs = parseInt(input, 10);
return isNaN(secs) ? null : secs;
};
const isParseSecs = (secs: number) => isNaN(secs) || secs < 0 || secs >= 60;
/**
* Formats seconds to MM:SS
*/
const formatSecondsToMMSS = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
export const SubmitIntroModal: React.FC<SubmitIntroModalProps> = ({
visible,
onClose,
currentTime,
imdbId,
season,
episode,
}) => {
const { t } = useTranslation();
const { width } = useWindowDimensions();
const { settings } = useSettings();
const [startTimeStr, setStartTimeStr] = useState('00:00');
const [endTimeStr, setEndTimeStr] = useState(formatSecondsToMMSS(currentTime));
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (visible) {
setEndTimeStr(formatSecondsToMMSS(currentTime));
}
}, [visible, currentTime]);
if (!visible) return null;
const handleCaptureStart = () => setStartTimeStr(formatSecondsToMMSS(currentTime));
const handleCaptureEnd = () => setEndTimeStr(formatSecondsToMMSS(currentTime));
const handleSubmit = async () => {
const startSec = parseTimeToSeconds(startTimeStr);
const endSec = parseTimeToSeconds(endTimeStr);
if (startSec === null || endSec === null) {
toastService.error('Invalid format', 'Please use MM:SS format');
return;
}
if (endSec <= startSec) {
toastService.warning('Invalid duration', 'End time must be after start time');
return;
}
if (!imdbId || season === undefined || episode === undefined) {
toastService.error('Missing metadata', 'Could not identify this episode');
return;
}
setIsSubmitting(true);
try {
const success = await introService.submitIntro(
settings.introDbApiKey,
imdbId,
season,
episode,
startSec,
endSec
);
if (success) {
toastService.success(t('player_ui.intro_submitted', { defaultValue: 'Intro submitted successfully' }));
onClose();
} else {
toastService.error(t('player_ui.intro_submit_failed', { defaultValue: 'Failed to submit intro' }));
}
} catch (error) {
toastService.error('Error', 'An unexpected error occurred');
} finally {
setIsSubmitting(false);
}
};
const startVal = parseTimeToSeconds(startTimeStr);
const endVal = parseTimeToSeconds(endTimeStr);
const durationSec = (startVal !== null && endVal !== null) ? endVal - startVal : 0;
return (
<View style={[StyleSheet.absoluteFill, { zIndex: 10000 }]}>
<TouchableOpacity
style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={onClose}
>
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }} />
</TouchableOpacity>
<View pointerEvents="box-none" style={localStyles.centeredView}>
<Animated.View
entering={SlideInDown.duration(300)}
exiting={SlideOutDown.duration(250)}
style={[localStyles.modalContainer, { width: Math.min(width * 0.85, 380) }]}
>
<View style={localStyles.header}>
<Text style={localStyles.title}>Submit Intro Timestamp</Text>
<TouchableOpacity onPress={onClose} style={localStyles.closeButton}>
<Ionicons name="close" size={24} color="rgba(255,255,255,0.5)" />
</TouchableOpacity>
</View>
<View style={localStyles.content}>
{/* Start Time Input */}
<View style={localStyles.inputRow}>
<View style={{ flex: 1 }}>
<Text style={localStyles.label}>Start Time (MM:SS)</Text>
<TextInput
style={localStyles.input}
value={startTimeStr}
onChangeText={setStartTimeStr}
placeholder="00:00"
placeholderTextColor="rgba(255,255,255,0.3)"
keyboardType="numbers-and-punctuation"
/>
</View>
<TouchableOpacity onPress={handleCaptureStart} style={localStyles.captureBtn}>
<MaterialIcons name="my-location" size={20} color="white" />
<Text style={localStyles.captureText}>Capture</Text>
</TouchableOpacity>
</View>
{/* End Time Input */}
<View style={localStyles.inputRow}>
<View style={{ flex: 1 }}>
<Text style={localStyles.label}>End Time (MM:SS)</Text>
<TextInput
style={localStyles.input}
value={endTimeStr}
onChangeText={setEndTimeStr}
placeholder="00:00"
placeholderTextColor="rgba(255,255,255,0.3)"
keyboardType="numbers-and-punctuation"
/>
</View>
<TouchableOpacity onPress={handleCaptureEnd} style={localStyles.captureBtn}>
<MaterialIcons name="my-location" size={20} color="white" />
<Text style={localStyles.captureText}>Capture</Text>
</TouchableOpacity>
</View>
{/* Action Buttons */}
<View style={localStyles.buttonRow}>
<TouchableOpacity
onPress={onClose}
disabled={isSubmitting}
style={[localStyles.cancelBtn, isSubmitting && { opacity: 0.5 }]}
>
<Text style={localStyles.cancelBtnText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleSubmit}
disabled={isSubmitting}
style={[localStyles.submitBtn, isSubmitting && { opacity: 0.7 }]}
>
{isSubmitting ? (
<ActivityIndicator color="black" />
) : (
<>
<MaterialIcons name="send" size={18} color="black" />
<Text style={localStyles.submitBtnText}>Submit</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</Animated.View>
</View>
</View>
);
};
const localStyles = StyleSheet.create({
centeredView: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 40,
},
modalContainer: {
backgroundColor: 'rgba(20, 20, 20, 0.98)',
borderRadius: 28,
padding: 24,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
shadowColor: "#000",
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.5,
shadowRadius: 15,
elevation: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
title: {
color: 'white',
fontSize: 18,
fontWeight: '700',
},
closeButton: {
padding: 4,
},
content: {
gap: 20,
},
inputRow: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 12,
},
label: {
color: 'rgba(255,255,255,0.6)',
fontSize: 12,
marginBottom: 8,
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
input: {
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
color: 'white',
fontSize: 16,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
captureBtn: {
backgroundColor: 'rgba(255,255,255,0.1)',
height: 48,
paddingHorizontal: 12,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
},
captureText: {
color: 'white',
fontSize: 13,
fontWeight: '600',
},
summaryBox: {
backgroundColor: 'rgba(255,255,255,0.03)',
borderRadius: 16,
padding: 16,
marginTop: 8,
},
summaryText: {
color: 'rgba(255,255,255,0.5)',
fontSize: 14,
marginBottom: 4,
},
hintText: {
color: 'rgba(255,255,255,0.3)',
fontSize: 12,
fontStyle: 'italic',
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
cancelBtn: {
flex: 1,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 16,
height: 56,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtnText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
submitBtn: {
flex: 2,
backgroundColor: 'white',
borderRadius: 16,
height: 56,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
},
submitBtnText: {
color: 'black',
fontSize: 16,
fontWeight: '700',
},
});

View file

@ -59,6 +59,8 @@ export interface AppSettings {
// Playback behavior // Playback behavior
alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85% alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85%
skipIntroEnabled: boolean; // Enable/disable Skip Intro overlay (IntroDB) skipIntroEnabled: boolean; // Enable/disable Skip Intro overlay (IntroDB)
introSubmitEnabled: boolean; // Enable/disable Intro Submission
introDbApiKey: string; // API Key for IntroDB submission
// Downloads // Downloads
enableDownloads: boolean; // Show Downloads tab and enable saving streams enableDownloads: boolean; // Show Downloads tab and enable saving streams
// Theme settings // Theme settings
@ -147,6 +149,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Playback behavior defaults // Playback behavior defaults
alwaysResume: true, alwaysResume: true,
skipIntroEnabled: true, skipIntroEnabled: true,
introSubmitEnabled: false,
introDbApiKey: '',
// Downloads // Downloads
enableDownloads: false, enableDownloads: false,
useExternalPlayerForDownloads: false, useExternalPlayerForDownloads: false,

View file

@ -417,7 +417,12 @@
"timing_offset": "Timing Offset (s)", "timing_offset": "Timing Offset (s)",
"visual_sync": "Visual Sync", "visual_sync": "Visual Sync",
"timing_hint": "Nudge subtitles earlier (-) or later (+) to sync if needed.", "timing_hint": "Nudge subtitles earlier (-) or later (+) to sync if needed.",
"reset_defaults": "Reset to defaults" "reset_defaults": "Reset to defaults",
"mark_intro_start": "Mark Intro Start",
"mark_intro_end": "Mark Intro End",
"intro_start_marked": "Intro start marked",
"intro_submitted": "Intro submitted successfully",
"intro_submit_failed": "Failed to submit intro"
}, },
"downloads": { "downloads": {
"title": "Downloads", "title": "Downloads",

View file

@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -13,6 +13,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SvgXml } from 'react-native-svg'; import { SvgXml } from 'react-native-svg';
import { toastService } from '../../services/toastService';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -77,6 +78,16 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
const config = useRealtimeConfig(); const config = useRealtimeConfig();
const [introDbLogoXml, setIntroDbLogoXml] = useState<string | null>(null); const [introDbLogoXml, setIntroDbLogoXml] = useState<string | null>(null);
const [apiKeyInput, setApiKeyInput] = useState(settings?.introDbApiKey || '');
useEffect(() => {
setApiKeyInput(settings?.introDbApiKey || '');
}, [settings?.introDbApiKey]);
const handleApiKeySubmit = () => {
updateSetting('introDbApiKey', apiKeyInput);
toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }));
};
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -225,6 +236,49 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
/> />
</SettingsCard> </SettingsCard>
{/* IntroDB Contribution Section */}
<SettingsCard title={t('settings.sections.introdb_contribution', { defaultValue: 'IntroDB Contribution' })} isTablet={isTablet}>
<SettingItem
title={t('settings.items.enable_intro_submission', { defaultValue: 'Enable Intro Submission' })}
description={t('settings.items.enable_intro_submission_desc', { defaultValue: 'Contribute timestamps to the community' })}
icon="flag"
renderControl={() => (
<CustomSwitch
value={settings?.introSubmitEnabled ?? false}
onValueChange={(value) => updateSetting('introSubmitEnabled', value)}
/>
)}
isLast={!settings?.introSubmitEnabled}
isTablet={isTablet}
/>
{settings?.introSubmitEnabled && (
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>
{t('settings.items.introdb_api_key', { defaultValue: 'INTRODB API KEY' })}
</Text>
<View style={styles.apiKeyRow}>
<TextInput
style={[styles.input, { flex: 1, marginRight: 10, color: currentTheme.colors.highEmphasis }]}
value={apiKeyInput}
onChangeText={setApiKeyInput}
placeholder="Enter your API key"
placeholderTextColor={currentTheme.colors.mediumEmphasis}
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
/>
<TouchableOpacity
style={styles.confirmButton}
onPress={handleApiKeySubmit}
>
<MaterialIcons name="check" size={24} color="black" />
</TouchableOpacity>
</View>
</View>
)}
</SettingsCard>
{/* Audio & Subtitle Preferences */} {/* Audio & Subtitle Preferences */}
<SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}> <SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}>
<SettingItem <SettingItem
@ -542,6 +596,39 @@ const styles = StyleSheet.create({
fontSize: 13, fontSize: 13,
color: 'rgba(255,255,255,0.5)', color: 'rgba(255,255,255,0.5)',
}, },
inputContainer: {
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 8,
},
inputLabel: {
fontSize: 12,
color: 'rgba(255,255,255,0.5)',
marginBottom: 8,
marginLeft: 4,
},
input: {
backgroundColor: 'rgba(255,255,255,0.08)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
color: 'white',
fontSize: 14,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
apiKeyRow: {
flexDirection: 'row',
alignItems: 'center',
},
confirmButton: {
backgroundColor: 'white',
borderRadius: 12,
width: 48,
height: 48,
justifyContent: 'center',
alignItems: 'center',
},
}); });
export default PlaybackSettingsScreen; export default PlaybackSettingsScreen;

View file

@ -187,6 +187,44 @@ async function fetchFromIntroDb(imdbId: string, season: number, episode: number)
} }
} }
/**
* Submits an intro timestamp to IntroDB
*/
export async function submitIntro(
apiKey: string,
imdbId: string,
season: number,
episode: number,
startTime: number, // in seconds
endTime: number // in seconds
): Promise<boolean> {
try {
if (!apiKey) {
logger.warn('[IntroService] Missing API key for submission');
return false;
}
const response = await axios.post(`${INTRODB_API_URL}/submit`, {
imdb_id: imdbId,
season,
episode,
start_ms: Math.round(startTime * 1000),
end_ms: Math.round(endTime * 1000),
}, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
timeout: 10000,
});
return response.status === 200 || response.status === 201;
} catch (error: any) {
logger.error('[IntroService] Error submitting intro:', error?.response?.data || error?.message || error);
return false;
}
}
/** /**
* Fetches skip intervals (intro, outro, recap) from available providers * Fetches skip intervals (intro, outro, recap) from available providers
*/ */
@ -266,7 +304,8 @@ export async function getIntroTimestamps(
export const introService = { export const introService = {
getIntroTimestamps, getIntroTimestamps,
getSkipTimes getSkipTimes,
submitIntro
}; };
export default introService; export default introService;

View file

@ -0,0 +1,45 @@
declare module '@kesha-antonov/react-native-background-downloader' {
export interface DownloadTask {
id: string;
percent: number;
bytesWritten: number;
totalBytes: number;
state: 'DOWNLOADING' | 'PAUSED' | 'DONE' | 'STOPPED' | 'FAILED';
pause(): void;
resume(): void;
stop(): void;
start(): void;
begin(handler: (expectedBytes: number) => void): DownloadTask;
progress(handler: (percent: number, bytesWritten: number, totalBytes: number) => void): DownloadTask;
done(handler: () => void): DownloadTask;
error(handler: (error: any) => void): DownloadTask;
}
export const directories: {
documents: string;
};
export function download(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
export function checkForExistingDownloads(): Promise<DownloadTask[]>;
// Legacy exports to match library behavior
export function completeHandler(id: string): void;
export function createDownloadTask(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
export function getExistingDownloadTasks(): Promise<DownloadTask[]>;
const RNBackgroundDownloader: {
download(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
checkForExistingDownloads(): Promise<DownloadTask[]>;
directories: {
documents: string;
};
completeHandler(id: string): void;
createDownloadTask(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
getExistingDownloadTasks(): Promise<DownloadTask[]>;
};
export default RNBackgroundDownloader;
}