VideoPlayer fixes

This commit is contained in:
Nayif Noushad 2025-04-12 04:07:35 +05:30
parent 83701955df
commit 6e62e4cb9b
39 changed files with 2250 additions and 679 deletions

BIN
assets/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

20
metro.config.js Normal file
View file

@ -0,0 +1,20 @@
const { getDefaultConfig } = require('expo/metro-config');
module.exports = (() => {
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config;
config.transformer = {
...transformer,
babelTransformerPath: require.resolve('react-native-svg-transformer'),
};
config.resolver = {
...resolver,
assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...resolver.sourceExts, 'svg'],
};
return config;
})();

818
package-lock.json generated
View file

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/slider": "^4.5.6",
"@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
@ -22,6 +23,7 @@
"expo": "~52.0.43", "expo": "~52.0.43",
"expo-av": "^15.0.2", "expo-av": "^15.0.2",
"expo-blur": "^14.0.3", "expo-blur": "^14.0.3",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7", "expo-image": "~2.0.7",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-notifications": "~0.29.14", "expo-notifications": "~0.29.14",
@ -40,12 +42,14 @@
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-svg": "^15.8.0",
"react-native-video": "^6.12.0" "react-native-video": "^6.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@types/react": "~18.3.12", "@types/react": "~18.3.12",
"@types/react-native": "^0.72.8", "@types/react-native": "^0.72.8",
"react-native-svg-transformer": "^1.5.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@ -3257,6 +3261,12 @@
"react-native": "^0.0.0-0 || >=0.60 <1.0" "react-native": "^0.0.0-0 || >=0.60 <1.0"
} }
}, },
"node_modules/@react-native-community/slider": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",
"integrity": "sha512-UhLPFeqx0YfPLrEz8ffT3uqAyXWu6iqFjohNsbp4cOU7hnJwg2RXtDnYHoHMr7MOkZDVdlLMdrSrAuzY6KGqrg==",
"license": "MIT"
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.76.9", "version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
@ -3851,6 +3861,462 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
"integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
"integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
"integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz",
"integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-svg-dynamic-title": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz",
"integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-svg-em-dimensions": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz",
"integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-transform-react-native-svg": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
"integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-transform-svg-component": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz",
"integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-preset": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
"integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
"@svgr/babel-plugin-remove-jsx-attribute": "8.0.0",
"@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0",
"@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0",
"@svgr/babel-plugin-svg-dynamic-title": "8.0.0",
"@svgr/babel-plugin-svg-em-dimensions": "8.0.0",
"@svgr/babel-plugin-transform-react-native-svg": "8.1.0",
"@svgr/babel-plugin-transform-svg-component": "8.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/core": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
"camelcase": "^6.2.0",
"cosmiconfig": "^8.1.3",
"snake-case": "^3.0.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@svgr/core/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/@svgr/core/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@svgr/core/node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0",
"path-type": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@svgr/core/node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@svgr/core/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@svgr/core/node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@svgr/core/node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@svgr/hast-util-to-babel-ast": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz",
"integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.21.3",
"entities": "^4.4.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@svgr/plugin-jsx": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
"integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
"@svgr/hast-util-to-babel-ast": "8.0.0",
"svg-parser": "^2.0.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@svgr/core": "*"
}
},
"node_modules/@svgr/plugin-svgo": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz",
"integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cosmiconfig": "^8.1.3",
"deepmerge": "^4.3.1",
"svgo": "^3.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@svgr/core": "*"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0",
"path-type": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@svgr/plugin-svgo/node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -4595,6 +5061,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@ -5315,6 +5787,92 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"css-tree": "~2.2.0"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -5517,6 +6075,72 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -5600,6 +6224,18 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -5980,6 +6616,15 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/expo-haptics": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.0.1.tgz",
"integrity": "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image": { "node_modules/expo-image": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.0.7.tgz", "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.0.7.tgz",
@ -7554,6 +8199,13 @@
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -7989,6 +8641,16 @@
"loose-envify": "cli.js" "loose-envify": "cli.js"
} }
}, },
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -8070,6 +8732,12 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -8741,6 +9409,17 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/node-dir": { "node_modules/node-dir": {
"version": "0.1.17", "version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
@ -8851,6 +9530,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -9159,6 +9850,29 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parent-module/node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": { "node_modules/parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@ -9193,6 +9907,13 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
"integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==",
"dev": true,
"license": "MIT"
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -9907,6 +10628,38 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-svg": {
"version": "15.8.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz",
"integrity": "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-svg-transformer": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.0.tgz",
"integrity": "sha512-RG5fSWJT7mjCQYocgYFUo1KYPLOoypPVG5LQab+pZZO7m4ciGaQIe0mhok3W4R5jLQsEXKo0u+aQGkZV/bZG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"path-dirname": "^1.0.2"
},
"peerDependencies": {
"react-native": ">=0.59.0",
"react-native-svg": ">=12.0.0"
}
},
"node_modules/react-native-vector-icons": { "node_modules/react-native-vector-icons": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
@ -10800,6 +11553,17 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -11140,6 +11904,60 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
"dev": true,
"license": "MIT"
},
"node_modules/svgo": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.3.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
},
"bin": {
"svgo": "bin/svgo"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/svgo"
}
},
"node_modules/svgo/node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/svgo/node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/tar": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",

View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/slider": "^4.5.6",
"@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
@ -23,6 +24,7 @@
"expo": "~52.0.43", "expo": "~52.0.43",
"expo-av": "^15.0.2", "expo-av": "^15.0.2",
"expo-blur": "^14.0.3", "expo-blur": "^14.0.3",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7", "expo-image": "~2.0.7",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-notifications": "~0.29.14", "expo-notifications": "~0.29.14",
@ -41,12 +43,14 @@
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-svg": "^15.8.0",
"react-native-video": "^6.12.0" "react-native-video": "^6.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@types/react": "~18.3.12", "@types/react": "~18.3.12",
"@types/react-native": "^0.72.8", "@types/react-native": "^0.72.8",
"react-native-svg-transformer": "^1.5.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"private": true "private": true

View file

@ -0,0 +1,4 @@
<svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="11" height="14" rx="2" fill="#575757"/>
<path d="M5.334 9.096C4.918 9.096 4.554 9.002 4.242 8.814C3.93 8.626 3.686 8.368 3.51 8.04C3.338 7.708 3.252 7.332 3.252 6.912C3.252 6.488 3.34 6.112 3.516 5.784C3.692 5.456 3.936 5.198 4.248 5.01C4.56 4.822 4.92 4.728 5.328 4.728C5.792 4.728 6.186 4.84 6.51 5.064C6.834 5.288 7.054 5.6 7.17 6H6.33C6.262 5.84 6.142 5.71 5.97 5.61C5.798 5.51 5.584 5.46 5.328 5.46C5.08 5.46 4.862 5.52 4.674 5.64C4.486 5.756 4.34 5.924 4.236 6.144C4.132 6.36 4.08 6.616 4.08 6.912C4.08 7.208 4.132 7.464 4.236 7.68C4.34 7.896 4.488 8.064 4.68 8.184C4.872 8.304 5.098 8.364 5.358 8.364C5.57 8.364 5.758 8.322 5.922 8.238C6.086 8.154 6.214 8.04 6.306 7.896C6.398 7.752 6.444 7.592 6.444 7.416V7.35H5.328V6.654H7.248V7.236C7.248 7.588 7.17 7.904 7.014 8.184C6.862 8.464 6.642 8.686 6.354 8.85C6.07 9.014 5.73 9.096 5.334 9.096Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 982 B

View file

@ -0,0 +1,4 @@
<svg width="28" height="14" viewBox="0 0 28 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="14" rx="2" fill="#575757"/>
<path d="M3.474 9V4.824H4.266L6.336 7.752V4.824H7.128V9H6.336L4.266 6.072V9H3.474ZM8.45161 6.912C8.45161 6.488 8.53561 6.112 8.70361 5.784C8.87561 5.452 9.11161 5.194 9.41161 5.01C9.71561 4.822 10.0656 4.728 10.4616 4.728C10.8176 4.728 11.1216 4.792 11.3736 4.92C11.6256 5.044 11.8276 5.216 11.9796 5.436C12.1316 5.656 12.2376 5.908 12.2976 6.192H11.4516C11.3996 5.968 11.2876 5.79 11.1156 5.658C10.9476 5.526 10.7296 5.46 10.4616 5.46C10.2256 5.46 10.0196 5.52 9.84361 5.64C9.66761 5.756 9.52961 5.924 9.42961 6.144C9.32961 6.36 9.27961 6.616 9.27961 6.912C9.27961 7.208 9.32961 7.466 9.42961 7.686C9.52961 7.902 9.66761 8.07 9.84361 8.19C10.0196 8.306 10.2256 8.364 10.4616 8.364C10.7296 8.364 10.9516 8.3 11.1276 8.172C11.3036 8.044 11.4156 7.874 11.4636 7.662H12.2976C12.2416 7.938 12.1356 8.184 11.9796 8.4C11.8276 8.616 11.6256 8.786 11.3736 8.91C11.1216 9.034 10.8176 9.096 10.4616 9.096C10.0656 9.096 9.71561 9.004 9.41161 8.82C9.11161 8.632 8.87561 8.374 8.70361 8.046C8.53561 7.714 8.45161 7.336 8.45161 6.912ZM13.574 7.59V6.936H15.566V7.59H13.574ZM18.3282 9V5.646L17.3322 5.952V5.25L19.1202 4.746V9H18.3282ZM24.228 4.824V5.466L22.662 9H21.816L23.4 5.526H21.234V4.824H24.228Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,4 @@
<svg width="28" height="14" viewBox="0 0 28 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="14" rx="2" fill="#575757"/>
<path d="M3.474 9V4.824H5.124C5.648 4.824 6.054 4.952 6.342 5.208C6.63 5.464 6.774 5.802 6.774 6.222C6.774 6.646 6.63 6.986 6.342 7.242C6.054 7.498 5.648 7.626 5.124 7.626H4.29V9H3.474ZM4.29 6.918H5.016C5.336 6.918 5.57 6.858 5.718 6.738C5.87 6.618 5.946 6.446 5.946 6.222C5.946 6.002 5.87 5.832 5.718 5.712C5.57 5.592 5.336 5.532 5.016 5.532H4.29V6.918ZM9.91252 9.096C9.49652 9.096 9.13252 9.002 8.82052 8.814C8.50852 8.626 8.26452 8.368 8.08852 8.04C7.91652 7.708 7.83052 7.332 7.83052 6.912C7.83052 6.488 7.91852 6.112 8.09452 5.784C8.27052 5.456 8.51452 5.198 8.82652 5.01C9.13852 4.822 9.49852 4.728 9.90652 4.728C10.3705 4.728 10.7645 4.84 11.0885 5.064C11.4125 5.288 11.6325 5.6 11.7485 6H10.9085C10.8405 5.84 10.7205 5.71 10.5485 5.61C10.3765 5.51 10.1625 5.46 9.90652 5.46C9.65852 5.46 9.44052 5.52 9.25252 5.64C9.06452 5.756 8.91852 5.924 8.81452 6.144C8.71052 6.36 8.65852 6.616 8.65852 6.912C8.65852 7.208 8.71052 7.464 8.81452 7.68C8.91852 7.896 9.06652 8.064 9.25852 8.184C9.45052 8.304 9.67652 8.364 9.93652 8.364C10.1485 8.364 10.3365 8.322 10.5005 8.238C10.6645 8.154 10.7925 8.04 10.8845 7.896C10.9765 7.752 11.0225 7.592 11.0225 7.416V7.35H9.90652V6.654H11.8265V7.236C11.8265 7.588 11.7485 7.904 11.5925 8.184C11.4405 8.464 11.2205 8.686 10.9325 8.85C10.6485 9.014 10.3085 9.096 9.91252 9.096ZM13.1462 7.59V6.936H15.1382V7.59H13.1462ZM17.9005 9V5.646L16.9045 5.952V5.25L18.6925 4.746V9H17.9005ZM22.3183 9.09C21.8623 9.09 21.4943 8.988 21.2143 8.784C20.9383 8.576 20.7663 8.292 20.6983 7.932H21.4903C21.5423 8.08 21.6403 8.194 21.7843 8.274C21.9323 8.35 22.1083 8.388 22.3123 8.388C22.5083 8.388 22.6803 8.34 22.8283 8.244C22.9763 8.148 23.0503 7.994 23.0503 7.782C23.0503 7.598 22.9803 7.46 22.8403 7.368C22.7003 7.272 22.5183 7.224 22.2943 7.224H21.7783V6.546H22.2643C22.4683 6.546 22.6343 6.496 22.7623 6.396C22.8903 6.296 22.9543 6.16 22.9543 5.988C22.9543 5.8 22.8863 5.662 22.7503 5.574C22.6143 5.482 22.4483 5.436 22.2523 5.436C22.0643 5.436 21.9043 5.476 21.7723 5.556C21.6403 5.636 21.5523 5.744 21.5083 5.88H20.7403C20.8243 5.536 21.0003 5.26 21.2683 5.052C21.5363 4.84 21.8803 4.734 22.3003 4.734C22.5603 4.734 22.8003 4.784 23.0203 4.884C23.2403 4.98 23.4143 5.118 23.5423 5.298C23.6743 5.478 23.7403 5.694 23.7403 5.946C23.7403 6.158 23.6843 6.342 23.5723 6.498C23.4603 6.654 23.3003 6.77 23.0923 6.846C23.3403 6.922 23.5283 7.05 23.6563 7.23C23.7883 7.406 23.8543 7.608 23.8543 7.836C23.8543 8.1 23.7843 8.326 23.6443 8.514C23.5083 8.702 23.3243 8.846 23.0923 8.946C22.8603 9.042 22.6023 9.09 22.3183 9.09Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,5 @@
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="13" rx="1.5" fill="#575757"/>
<rect x="0.5" y="0.5" width="15" height="13" rx="1.5" stroke="black"/>
<path d="M3.474 9V4.824H5.124C5.648 4.824 6.054 4.952 6.342 5.208C6.63 5.464 6.774 5.802 6.774 6.222C6.774 6.646 6.63 6.986 6.342 7.242C6.054 7.498 5.648 7.626 5.124 7.626H4.29V9H3.474ZM4.29 6.918H5.016C5.336 6.918 5.57 6.858 5.718 6.738C5.87 6.618 5.946 6.446 5.946 6.222C5.946 6.002 5.87 5.832 5.718 5.712C5.57 5.592 5.336 5.532 5.016 5.532H4.29V6.918ZM9.91252 9.096C9.49652 9.096 9.13252 9.002 8.82052 8.814C8.50852 8.626 8.26452 8.368 8.08852 8.04C7.91652 7.708 7.83052 7.332 7.83052 6.912C7.83052 6.488 7.91852 6.112 8.09452 5.784C8.27052 5.456 8.51452 5.198 8.82652 5.01C9.13852 4.822 9.49852 4.728 9.90652 4.728C10.3705 4.728 10.7645 4.84 11.0885 5.064C11.4125 5.288 11.6325 5.6 11.7485 6H10.9085C10.8405 5.84 10.7205 5.71 10.5485 5.61C10.3765 5.51 10.1625 5.46 9.90652 5.46C9.65852 5.46 9.44052 5.52 9.25252 5.64C9.06452 5.756 8.91852 5.924 8.81452 6.144C8.71052 6.36 8.65852 6.616 8.65852 6.912C8.65852 7.208 8.71052 7.464 8.81452 7.68C8.91852 7.896 9.06652 8.064 9.25852 8.184C9.45052 8.304 9.67652 8.364 9.93652 8.364C10.1485 8.364 10.3365 8.322 10.5005 8.238C10.6645 8.154 10.7925 8.04 10.8845 7.896C10.9765 7.752 11.0225 7.592 11.0225 7.416V7.35H9.90652V6.654H11.8265V7.236C11.8265 7.588 11.7485 7.904 11.5925 8.184C11.4405 8.464 11.2205 8.686 10.9325 8.85C10.6485 9.014 10.3085 9.096 9.91252 9.096Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,4 @@
<svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="11" height="14" rx="2" fill="#575757"/>
<path d="M3.474 9V4.824H5.178C5.69 4.824 6.088 4.946 6.372 5.19C6.66 5.434 6.804 5.764 6.804 6.18C6.804 6.48 6.726 6.736 6.57 6.948C6.418 7.16 6.214 7.318 5.958 7.422L6.99 9H6.042L5.124 7.554H4.29V9H3.474ZM4.29 6.846H5.07C5.382 6.846 5.61 6.79 5.754 6.678C5.902 6.562 5.976 6.394 5.976 6.174C5.976 5.954 5.902 5.792 5.754 5.688C5.61 5.584 5.382 5.532 5.07 5.532H4.29V6.846Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View file

@ -0,0 +1,4 @@
<svg width="27" height="14" viewBox="0 0 27 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="27" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM17.5548 9V5.646L16.5588 5.952V5.25L18.3468 4.746V9H17.5548ZM22.2786 9V8.052H20.2566V7.446L22.2786 4.824H23.0466V7.392H23.6586V8.052H23.0466V9H22.2786ZM21.0726 7.392H22.2786V5.838L21.0726 7.392Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="14" viewBox="0 0 24 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM18.1848 9.096C17.7688 9.096 17.4048 9.002 17.0928 8.814C16.7808 8.626 16.5368 8.368 16.3608 8.04C16.1888 7.708 16.1028 7.332 16.1028 6.912C16.1028 6.488 16.1908 6.112 16.3668 5.784C16.5428 5.456 16.7868 5.198 17.0988 5.01C17.4108 4.822 17.7708 4.728 18.1788 4.728C18.6428 4.728 19.0368 4.84 19.3608 5.064C19.6848 5.288 19.9048 5.6 20.0208 6H19.1808C19.1128 5.84 18.9928 5.71 18.8208 5.61C18.6488 5.51 18.4348 5.46 18.1788 5.46C17.9308 5.46 17.7128 5.52 17.5248 5.64C17.3368 5.756 17.1908 5.924 17.0868 6.144C16.9828 6.36 16.9308 6.616 16.9308 6.912C16.9308 7.208 16.9828 7.464 17.0868 7.68C17.1908 7.896 17.3388 8.064 17.5308 8.184C17.7228 8.304 17.9488 8.364 18.2088 8.364C18.4208 8.364 18.6088 8.322 18.7728 8.238C18.9368 8.154 19.0648 8.04 19.1568 7.896C19.2488 7.752 19.2948 7.592 19.2948 7.416V7.35H18.1788V6.654H20.0988V7.236C20.0988 7.588 20.0208 7.904 19.8648 8.184C19.7128 8.464 19.4928 8.686 19.2048 8.85C18.9208 9.014 18.5808 9.096 18.1848 9.096Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,4 @@
<svg width="30" height="14" viewBox="0 0 30 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM17.1048 9H16.3248V4.824H17.2368L18.5508 6.96L19.8348 4.824H20.7468V9H19.9608V5.97L18.5328 8.316L17.1048 5.976V9ZM25.112 9L24.764 8.052H23.078L22.736 9H21.902L23.516 4.824H24.362L25.976 9H25.112ZM23.336 7.344H24.512L23.924 5.73L23.336 7.344Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

View file

@ -0,0 +1,4 @@
<svg width="28" height="14" viewBox="0 0 28 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM16.3248 9V4.824H17.9748C18.4988 4.824 18.9048 4.952 19.1928 5.208C19.4808 5.464 19.6248 5.802 19.6248 6.222C19.6248 6.646 19.4808 6.986 19.1928 7.242C18.9048 7.498 18.4988 7.626 17.9748 7.626H17.1408V9H16.3248ZM17.1408 6.918H17.8668C18.1868 6.918 18.4208 6.858 18.5688 6.738C18.7208 6.618 18.7968 6.446 18.7968 6.222C18.7968 6.002 18.7208 5.832 18.5688 5.712C18.4208 5.592 18.1868 5.532 17.8668 5.532H17.1408V6.918ZM22.7633 9.096C22.3473 9.096 21.9833 9.002 21.6713 8.814C21.3593 8.626 21.1153 8.368 20.9393 8.04C20.7673 7.708 20.6813 7.332 20.6813 6.912C20.6813 6.488 20.7693 6.112 20.9453 5.784C21.1213 5.456 21.3653 5.198 21.6773 5.01C21.9893 4.822 22.3493 4.728 22.7573 4.728C23.2213 4.728 23.6153 4.84 23.9393 5.064C24.2633 5.288 24.4833 5.6 24.5993 6H23.7593C23.6913 5.84 23.5713 5.71 23.3993 5.61C23.2273 5.51 23.0133 5.46 22.7573 5.46C22.5093 5.46 22.2913 5.52 22.1033 5.64C21.9153 5.756 21.7693 5.924 21.6653 6.144C21.5613 6.36 21.5093 6.616 21.5093 6.912C21.5093 7.208 21.5613 7.464 21.6653 7.68C21.7693 7.896 21.9173 8.064 22.1093 8.184C22.3013 8.304 22.5273 8.364 22.7873 8.364C22.9993 8.364 23.1873 8.322 23.3513 8.238C23.5153 8.154 23.6433 8.04 23.7353 7.896C23.8273 7.752 23.8733 7.592 23.8733 7.416V7.35H22.7573V6.654H24.6773V7.236C24.6773 7.588 24.5993 7.904 24.4433 8.184C24.2913 8.464 24.0713 8.686 23.7833 8.85C23.4993 9.014 23.1593 9.096 22.7633 9.096Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,4 @@
<svg width="23" height="14" viewBox="0 0 23 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="23" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM17.0954 9V7.434L15.5654 4.824H16.4954L17.5214 6.672L18.5474 4.824H19.4414L17.9114 7.434V9H17.0954Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 466 B

View file

@ -0,0 +1,4 @@
<svg width="27" height="14" viewBox="0 0 27 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="27" height="14" rx="2" fill="#575757"/>
<path d="M5.43 9H4.614V5.538H3.222V4.824H6.822V5.538H5.43V9ZM11.8123 4.824L10.2103 9H9.35827L7.75627 4.824H8.64427L9.80227 8.016L10.9543 4.824H11.8123ZM12.8005 7.59V6.936H14.7925V7.59H12.8005ZM17.0954 9V7.434L15.5654 4.824H16.4954L17.5214 6.672L18.5474 4.824H19.4414L17.9114 7.434V9H17.0954ZM23.4429 4.824V5.466L21.8769 9H21.0309L22.6149 5.526H20.4489V4.824H23.4429Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

BIN
src/assets/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,7 @@
<svg width="25" height="14" viewBox="0 0 25 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 12.0001L7 2.00006H10V12.0001H7.5V11.0001H4.5L3.5 12.0001H0ZM7.5 5.50006L5.5 8.50006H7.5V5.50006Z" fill="#CBCBCB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 2.00006V12.0001H15.5C16.5 11.8334 18.5 11.0001 18.5 7.00006C18.3333 5.33339 17.3 2.00006 14.5 2.00006H10.5ZM13 9.00006V5.00006C15 5.00006 15.5 6.33339 15.5 7.00006C15.5 8.60006 14.8333 9.00006 14.5 9.00006H13Z" fill="#CBCBCB"/>
<path d="M18 2.00006C20 3 20 11 18 12.0001H18.5C21 10.5 21 3.5 18.5 2L18 2.00006Z" fill="#CBCBCB"/>
<path d="M20 2.00006C22 3 22 11 20 12.0001H20.5C23 10.5 23 3.5 20.5 2L20 2.00006Z" fill="#CBCBCB"/>
<path d="M22 2.00006C24 3 24 11 22 12.0001H22.5C25 10.5 25 3.5 22.5 2L22 2.00006Z" fill="#CBCBCB"/>
</svg>

After

Width:  |  Height:  |  Size: 853 B

View file

@ -0,0 +1,5 @@
<svg width="17" height="14" viewBox="0 0 17 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="16" height="13" rx="1.5" stroke="#575757"/>
<path d="M3 10.0002V4.43224H4.088V6.60824H6.8V4.43224H7.888V10.0002H6.8V7.56024H4.088V10.0002H3Z" fill="#CBCBCB"/>
<path d="M9.39844 10.0002V4.43224H11.5024C12.0571 4.43224 12.5424 4.54691 12.9584 4.77624C13.3798 5.00558 13.7078 5.32824 13.9424 5.74424C14.1824 6.16024 14.3024 6.65091 14.3024 7.21624C14.3024 7.78158 14.1824 8.27224 13.9424 8.68824C13.7078 9.10424 13.3798 9.42691 12.9584 9.65624C12.5424 9.88558 12.0571 10.0002 11.5024 10.0002H9.39844ZM10.4864 9.04824H11.4384C11.7798 9.04824 12.0811 8.97624 12.3424 8.83224C12.6091 8.68824 12.8171 8.48024 12.9664 8.20824C13.1211 7.93624 13.1984 7.60558 13.1984 7.21624C13.1984 6.82691 13.1211 6.49624 12.9664 6.22424C12.8171 5.95224 12.6091 5.74424 12.3424 5.60024C12.0811 5.45624 11.7798 5.38424 11.4384 5.38424H10.4864V9.04824Z" fill="#CBCBCB"/>
</svg>

After

Width:  |  Height:  |  Size: 977 B

View file

@ -0,0 +1,10 @@
<svg width="50" height="14" viewBox="0 0 50 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="13" rx="1.5" stroke="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9999 3.5H3V10.5H11.9999V3.5ZM4.49995 9.5C4.32871 9.5 4.16151 9.48267 3.99995 9.44995V4.55005C4.10415 4.52905 4.21069 4.51416 4.31915 4.50635C4.33529 4.50513 4.35146 4.50439 4.36768 4.50366L4.40562 4.50195C4.43691 4.50073 4.46836 4.5 4.49995 4.5C5.88065 4.5 6.99995 5.61938 6.99995 7C6.99995 8.38062 5.88065 9.5 4.49995 9.5ZM10.4999 9.5C10.5348 9.5 10.5695 9.49927 10.6041 9.4978C10.6614 9.49561 10.7183 9.49146 10.7746 9.48535C10.8508 9.47681 10.926 9.46509 10.9999 9.44995V4.55005C10.9079 4.53149 10.814 4.51782 10.7187 4.50952C10.6466 4.50317 10.5736 4.5 10.4999 4.5C9.11924 4.5 7.99995 5.61938 7.99995 7C7.99995 8.38062 9.11924 9.5 10.4999 9.5Z" fill="#CBCBCB"/>
<path d="M20.2905 4.092L17.8875 10.356H16.6095L14.2065 4.092H15.5385L17.2755 8.88L19.0035 4.092H20.2905Z" fill="#CBCBCB"/>
<path d="M22.6024 10.356H21.3784V4.092H22.6024V10.356Z" fill="#CBCBCB"/>
<path d="M26.4969 10.5C25.7649 10.5 25.1679 10.332 24.7059 9.996C24.2499 9.66 23.9529 9.192 23.8149 8.592H25.0389C25.1169 8.844 25.2819 9.048 25.5339 9.204C25.7919 9.354 26.1069 9.429 26.4789 9.429C26.8329 9.429 27.1239 9.363 27.3519 9.231C27.5859 9.093 27.7029 8.895 27.7029 8.637C27.7029 8.487 27.6699 8.358 27.6039 8.25C27.5379 8.136 27.4149 8.04 27.2349 7.962C27.0549 7.878 26.7939 7.809 26.4519 7.755L25.7499 7.638C25.1679 7.542 24.7299 7.356 24.4359 7.08C24.1479 6.804 24.0039 6.408 24.0039 5.892C24.0039 5.496 24.1089 5.154 24.3189 4.866C24.5349 4.572 24.8229 4.347 25.1829 4.191C25.5489 4.029 25.9569 3.948 26.4069 3.948C27.0609 3.948 27.6099 4.104 28.0539 4.416C28.4979 4.722 28.7709 5.172 28.8729 5.766H27.6489C27.5889 5.514 27.4509 5.328 27.2349 5.208C27.0189 5.082 26.7459 5.019 26.4159 5.019C26.0499 5.019 25.7619 5.088 25.5519 5.226C25.3419 5.358 25.2369 5.544 25.2369 5.784C25.2369 6 25.3149 6.168 25.4709 6.288C25.6329 6.408 25.9509 6.507 26.4249 6.585L27.0999 6.693C28.3299 6.891 28.9449 7.5 28.9449 8.52C28.9449 8.94 28.8399 9.3 28.6299 9.6C28.4199 9.894 28.1289 10.119 27.7569 10.275C27.3909 10.425 26.9709 10.5 26.4969 10.5Z" fill="#CBCBCB"/>
<path d="M31.4959 10.356H30.2719V4.092H31.4959V10.356Z" fill="#CBCBCB"/>
<path d="M36.0024 10.5C35.5404 10.5 35.1144 10.419 34.7244 10.257C34.3404 10.095 34.0074 9.867 33.7254 9.573C33.4434 9.279 33.2244 8.934 33.0684 8.538C32.9124 8.136 32.8344 7.698 32.8344 7.224C32.8344 6.75 32.9124 6.315 33.0684 5.919C33.2244 5.517 33.4434 5.169 33.7254 4.875C34.0074 4.581 34.3404 4.353 34.7244 4.191C35.1144 4.029 35.5404 3.948 36.0024 3.948C36.4644 3.948 36.8874 4.029 37.2714 4.191C37.6614 4.353 37.9974 4.581 38.2794 4.875C38.5674 5.169 38.7894 5.517 38.9454 5.919C39.1014 6.315 39.1794 6.75 39.1794 7.224C39.1794 7.698 39.1014 8.136 38.9454 8.538C38.7894 8.934 38.5674 9.279 38.2794 9.573C37.9974 9.867 37.6614 10.095 37.2714 10.257C36.8874 10.419 36.4644 10.5 36.0024 10.5ZM36.0024 9.402C36.3804 9.402 36.7134 9.312 37.0014 9.132C37.2954 8.946 37.5234 8.691 37.6854 8.367C37.8534 8.037 37.9374 7.656 37.9374 7.224C37.9374 6.792 37.8534 6.414 37.6854 6.09C37.5234 5.76 37.2954 5.505 37.0014 5.325C36.7134 5.139 36.3804 5.046 36.0024 5.046C35.6304 5.046 35.2974 5.139 35.0034 5.325C34.7154 5.505 34.4874 5.76 34.3194 6.09C34.1574 6.414 34.0764 6.792 34.0764 7.224C34.0764 7.656 34.1574 8.037 34.3194 8.367C34.4874 8.691 34.7154 8.946 35.0034 9.132C35.2974 9.312 35.6304 9.402 36.0024 9.402Z" fill="#CBCBCB"/>
<path d="M40.519 10.356V4.092H41.707L44.812 8.484V4.092H46V10.356H44.812L41.707 5.964V10.356H40.519Z" fill="#CBCBCB"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -5,6 +5,7 @@ import { colors } from '../styles/colors';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '../navigation/AppNavigator'; import type { RootStackParamList } from '../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>; type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -13,28 +14,46 @@ export const NuvioHeader = () => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.contentContainer}> <LinearGradient
<Text style={styles.title}>NUVIO</Text> colors={[
<TouchableOpacity '#000000',
style={styles.searchButton} 'rgba(0, 0, 0, 0.95)',
onPress={() => navigation.navigate('Search')} 'rgba(0, 0, 0, 0.8)',
> 'rgba(0, 0, 0, 0.2)',
<MaterialCommunityIcons 'transparent'
name="magnify" ]}
size={28} locations={[0, 0.3, 0.6, 0.8, 1]}
color={colors.white} style={styles.gradient}
/> >
</TouchableOpacity> <View style={styles.contentContainer}>
</View> <Text style={styles.title}>NUVIO</Text>
<TouchableOpacity
style={styles.searchButton}
onPress={() => navigation.navigate('Search')}
>
<MaterialCommunityIcons
name="magnify"
size={28}
color={colors.white}
/>
</TouchableOpacity>
</View>
</LinearGradient>
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.darkBackground, position: 'absolute',
height: Platform.OS === 'ios' ? 96 : 80, top: 0,
paddingTop: Platform.OS === 'ios' ? 48 : 32, left: 0,
right: 0,
zIndex: 10,
},
gradient: {
height: Platform.OS === 'ios' ? 100 : 90,
paddingTop: Platform.OS === 'ios' ? 40 : 24,
}, },
contentContainer: { contentContainer: {
flexDirection: 'row', flexDirection: 'row',

View file

@ -147,9 +147,11 @@ export const ThisWeekSection = () => {
const handleEpisodePress = (episode: ThisWeekEpisode) => { const handleEpisodePress = (episode: ThisWeekEpisode) => {
// For upcoming episodes, go to the metadata screen // For upcoming episodes, go to the metadata screen
if (!episode.isReleased) { if (!episode.isReleased) {
const episodeId = `${episode.seriesId}:${episode.season}:${episode.episode}`;
navigation.navigate('Metadata', { navigation.navigate('Metadata', {
id: episode.seriesId, id: episode.seriesId,
type: 'series' type: 'series',
episodeId
}); });
return; return;
} }

View file

@ -0,0 +1,84 @@
import React from 'react';
import TVPGSvg from '../../assets/agerating/TV-PG.svg';
import PGSvg from '../../assets/agerating/PG.svg';
import TVGSvg from '../../assets/agerating/TV-G.svg';
import NC17Svg from '../../assets/agerating/NC-17.svg';
import GSvg from '../../assets/agerating/G.svg';
import TVMASvg from '../../assets/agerating/TV-MA.svg';
import TV13Svg from '../../assets/agerating/TV-13.svg';
import TVY7Svg from '../../assets/agerating/TV-Y7.svg';
import RSvg from '../../assets/agerating/R.svg';
import PG13Svg from '../../assets/agerating/PG-13.svg';
import TVYSvg from '../../assets/agerating/TV-Y.svg';
interface AgeBadgeProps {
rating: string;
}
const AgeBadge: React.FC<AgeBadgeProps> = ({ rating }) => {
// Normalize the rating to match our file names
const normalizeRating = (rating: string): string => {
// Convert to uppercase and remove any spaces
const normalized = rating.toUpperCase().replace(/\s+/g, '');
// Map some common variations
const ratingMap: { [key: string]: string } = {
'TVPG': 'TV-PG',
'TVG': 'TV-G',
'TVMA': 'TV-MA',
'TV14': 'TV-13',
'TVY7': 'TV-Y7',
'TVY': 'TV-Y',
'PG13': 'PG-13',
'NC17': 'NC-17',
};
return ratingMap[normalized] || normalized;
};
const getRatingComponent = (normalizedRating: string) => {
const svgProps = {
width: 32,
height: 32,
preserveAspectRatio: "xMidYMid meet"
};
switch (normalizedRating) {
case 'TV-PG':
return <TVPGSvg {...svgProps} />;
case 'PG':
return <PGSvg {...svgProps} />;
case 'TV-G':
return <TVGSvg {...svgProps} />;
case 'NC-17':
return <NC17Svg {...svgProps} />;
case 'G':
return <GSvg {...svgProps} />;
case 'TV-MA':
return <TVMASvg {...svgProps} />;
case 'TV-13':
return <TV13Svg {...svgProps} />;
case 'TV-Y7':
return <TVY7Svg {...svgProps} />;
case 'R':
return <RSvg {...svgProps} />;
case 'PG-13':
return <PG13Svg {...svgProps} />;
case 'TV-Y':
return <TVYSvg {...svgProps} />;
default:
return null;
}
};
const normalizedRating = normalizeRating(rating);
const RatingComponent = getRatingComponent(normalizedRating);
if (!RatingComponent) {
return null;
}
return RatingComponent;
};
export default AgeBadge;

View file

@ -0,0 +1,29 @@
import React from 'react';
import HDSvg from '../../assets/qualitybadge/HD.svg';
import VISIONSvg from '../../assets/qualitybadge/VISION.svg';
import ADSvg from '../../assets/qualitybadge/AD.svg';
interface QualityBadgeProps {
type: 'HD' | 'VISION' | 'AD';
}
const QualityBadge: React.FC<QualityBadgeProps> = ({ type }) => {
const svgProps = {
width: 32,
height: 32,
preserveAspectRatio: "xMidYMid meet"
};
switch (type) {
case 'HD':
return <HDSvg {...svgProps} />;
case 'VISION':
return <VISIONSvg {...svgProps} />;
case 'AD':
return <ADSvg {...svgProps} />;
default:
return null;
}
};
export default QualityBadge;

View file

@ -1,10 +1,12 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme } from 'react-native'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { colors } from '../../styles/colors'; import { colors } from '../../styles/colors';
import { Episode } from '../../types/metadata'; import { Episode } from '../../types/metadata';
import { tmdbService } from '../../services/tmdbService'; import { tmdbService } from '../../services/tmdbService';
import { storageService } from '../../services/storageService';
import { useFocusEffect } from '@react-navigation/native';
interface SeriesContentProps { interface SeriesContentProps {
episodes: Episode[]; episodes: Episode[];
@ -13,7 +15,7 @@ interface SeriesContentProps {
onSeasonChange: (season: number) => void; onSeasonChange: (season: number) => void;
onSelectEpisode: (episode: Episode) => void; onSelectEpisode: (episode: Episode) => void;
groupedEpisodes?: { [seasonNumber: number]: Episode[] }; groupedEpisodes?: { [seasonNumber: number]: Episode[] };
metadata?: { poster?: string }; metadata?: { poster?: string; id?: string };
} }
// Add placeholder constant at the top // Add placeholder constant at the top
@ -33,6 +35,39 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const isTablet = width > 768; const isTablet = width > 768;
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
const [episodeProgress, setEpisodeProgress] = useState<{ [key: string]: { currentTime: number; duration: number } }>({});
const loadEpisodesProgress = async () => {
if (!metadata?.id) return;
const allProgress = await storageService.getAllWatchProgress();
const progress: { [key: string]: { currentTime: number; duration: number } } = {};
episodes.forEach(episode => {
const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`;
const key = `series:${metadata.id}:${episodeId}`;
if (allProgress[key]) {
progress[episodeId] = {
currentTime: allProgress[key].currentTime,
duration: allProgress[key].duration
};
}
});
setEpisodeProgress(progress);
};
// Initial load of watch progress
useEffect(() => {
loadEpisodesProgress();
}, [episodes, metadata?.id]);
// Refresh watch progress when screen comes into focus
useFocusEffect(
React.useCallback(() => {
loadEpisodesProgress();
}, [episodes, metadata?.id])
);
if (loadingSeasons) { if (loadingSeasons) {
return ( return (
@ -134,7 +169,15 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
year: 'numeric' year: 'numeric'
}); });
}; };
// Get episode progress
const episodeId = episode.stremioId || `${metadata?.id}:${episode.season_number}:${episode.episode_number}`;
const progress = episodeProgress[episodeId];
const progressPercent = progress ? (progress.currentTime / progress.duration) * 100 : 0;
// Don't show progress bar if episode is complete (>= 95%)
const showProgress = progress && progressPercent < 95;
return ( return (
<TouchableOpacity <TouchableOpacity
key={episode.id} key={episode.id}
@ -151,6 +194,21 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<View style={styles.episodeNumberBadge}> <View style={styles.episodeNumberBadge}>
<Text style={styles.episodeNumberText}>{episodeString}</Text> <Text style={styles.episodeNumberText}>{episodeString}</Text>
</View> </View>
{showProgress && (
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{ width: `${progressPercent}%` }
]}
/>
</View>
)}
{progressPercent >= 95 && (
<View style={styles.completedBadge}>
<MaterialIcons name="check" size={12} color={colors.white} />
</View>
)}
</View> </View>
<View style={styles.episodeInfo}> <View style={styles.episodeInfo}>
@ -398,4 +456,44 @@ const styles = StyleSheet.create({
color: colors.text, color: colors.text,
fontWeight: '700', fontWeight: '700',
}, },
progressBarContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 3,
backgroundColor: 'rgba(0,0,0,0.5)',
},
progressBar: {
height: '100%',
backgroundColor: colors.primary,
},
progressTextContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.7)',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 4,
marginRight: 8,
},
progressText: {
color: colors.primary,
fontSize: 12,
fontWeight: '600',
marginLeft: 4,
},
completedBadge: {
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: colors.success,
width: 20,
height: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.3)',
},
}); });

View file

@ -795,6 +795,14 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
const fetchedTmdbId = await tmdbService.extractTMDBIdFromStremioId(id); const fetchedTmdbId = await tmdbService.extractTMDBIdFromStremioId(id);
if (fetchedTmdbId) { if (fetchedTmdbId) {
setTmdbId(fetchedTmdbId); setTmdbId(fetchedTmdbId);
// Fetch certification
const certification = await tmdbService.getCertification(type, fetchedTmdbId);
if (certification) {
setMetadata(prev => prev ? {
...prev,
certification
} : null);
}
} else { } else {
console.warn('Could not determine TMDB ID for recommendations.'); console.warn('Could not determine TMDB ID for recommendations.');
} }

View file

@ -10,6 +10,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { colors } from '../styles/colors'; import { colors } from '../styles/colors';
import { NuvioHeader } from '../components/NuvioHeader'; import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams';
// Import screens with their proper types // Import screens with their proper types
import HomeScreen from '../screens/HomeScreen'; import HomeScreen from '../screens/HomeScreen';
@ -30,15 +31,50 @@ import NotificationSettingsScreen from '../screens/NotificationSettingsScreen';
// Stack navigator types // Stack navigator types
export type RootStackParamList = { export type RootStackParamList = {
MainTabs: undefined; MainTabs: undefined;
Metadata: { id: string; type: string }; Home: undefined;
Streams: { id: string; type: string; episodeId?: string }; Discover: undefined;
Player: { uri: string; title?: string; season?: number; episode?: number; episodeTitle?: string; quality?: string; year?: number; streamProvider?: string }; Library: undefined;
Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string }; Settings: undefined;
Addons: undefined;
Search: undefined; Search: undefined;
ShowRatings: { showId: number };
CatalogSettings: undefined;
Calendar: undefined; Calendar: undefined;
Metadata: {
id: string;
type: string;
episodeId?: string;
};
Streams: {
id: string;
type: string;
episodeId?: string;
};
VideoPlayer: {
id: string;
type: string;
stream: Stream;
episodeId?: string;
};
Player: {
uri: string;
title?: string;
season?: number;
episode?: number;
episodeTitle?: string;
quality?: string;
year?: number;
streamProvider?: string;
id?: string;
type?: string;
episodeId?: string;
};
Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string };
Credits: { mediaId: string; mediaType: string };
ShowRatings: { showId: number };
Account: undefined;
Payment: undefined;
PrivacyPolicy: undefined;
About: undefined;
Addons: undefined;
CatalogSettings: undefined;
NotificationSettings: undefined; NotificationSettings: undefined;
}; };
@ -448,7 +484,7 @@ const MainTabs = () => {
}} }}
/> />
), ),
header: () => route.name === 'Home' ? <NuvioHeader routeName={route.name} /> : null, header: () => route.name === 'Home' ? <NuvioHeader /> : null,
headerShown: route.name === 'Home', headerShown: route.name === 'Home',
})} })}
> >

View file

@ -229,17 +229,18 @@ const CalendarScreen = () => {
fetchCalendarData(); fetchCalendarData();
}, [fetchCalendarData]); }, [fetchCalendarData]);
const handleSeriesPress = useCallback((seriesId: string) => { const handleSeriesPress = useCallback((seriesId: string, episode?: CalendarEpisode) => {
navigation.navigate('Metadata', { navigation.navigate('Metadata', {
id: seriesId, id: seriesId,
type: 'series' type: 'series',
episodeId: episode ? `${episode.seriesId}:${episode.season}:${episode.episode}` : undefined
}); });
}, [navigation]); }, [navigation]);
const handleEpisodePress = useCallback((episode: CalendarEpisode) => { const handleEpisodePress = useCallback((episode: CalendarEpisode) => {
// For series without episode dates, just go to the series page // For series without episode dates, just go to the series page
if (!episode.releaseDate) { if (!episode.releaseDate) {
handleSeriesPress(episode.seriesId); handleSeriesPress(episode.seriesId, episode);
return; return;
} }
@ -273,7 +274,7 @@ const CalendarScreen = () => {
activeOpacity={0.7} activeOpacity={0.7}
> >
<TouchableOpacity <TouchableOpacity
onPress={() => handleSeriesPress(item.seriesId)} onPress={() => handleSeriesPress(item.seriesId, item)}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Image <Image

View file

@ -15,7 +15,6 @@ import {
ScrollView, ScrollView,
Platform, Platform,
Image, Image,
Vibration,
Modal, Modal,
Pressable Pressable
} from 'react-native'; } from 'react-native';
@ -48,6 +47,7 @@ import {
} from 'react-native-gesture-handler'; } from 'react-native-gesture-handler';
import { useCatalogContext } from '../contexts/CatalogContext'; import { useCatalogContext } from '../contexts/CatalogContext';
import { ThisWeekSection } from '../components/home/ThisWeekSection'; import { ThisWeekSection } from '../components/home/ThisWeekSection';
import * as Haptics from 'expo-haptics';
// Define interfaces for our data // Define interfaces for our data
interface Category { interface Category {
@ -216,7 +216,6 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {
setMenuVisible(true); setMenuVisible(true);
Vibration.vibrate(50);
}, []); }, []);
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
@ -366,6 +365,16 @@ const HomeScreen = () => {
const [loadingImages, setLoadingImages] = useState(true); const [loadingImages, setLoadingImages] = useState(true);
const maxRetries = 3; const maxRetries = 3;
const { lastUpdate } = useCatalogContext(); const { lastUpdate } = useCatalogContext();
const [isSaved, setIsSaved] = useState(false);
useEffect(() => {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
return () => {
StatusBar.setTranslucent(false);
StatusBar.setBackgroundColor(colors.darkBackground);
};
}, []);
// Pre-warm the metadata screen // Pre-warm the metadata screen
useEffect(() => { useEffect(() => {
@ -522,6 +531,43 @@ const HomeScreen = () => {
loadContent(true); loadContent(true);
}, [loadContent, lastUpdate]); }, [loadContent, lastUpdate]);
// Check if content is in library
useEffect(() => {
if (featuredContent) {
const checkLibrary = async () => {
const items = await catalogService.getLibraryItems();
setIsSaved(items.some(item => item.id === featuredContent.id));
};
checkLibrary();
}
}, [featuredContent]);
// Subscribe to library updates
useEffect(() => {
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
if (featuredContent) {
setIsSaved(items.some(item => item.id === featuredContent.id));
}
});
return () => unsubscribe();
}, [featuredContent]);
const handleSaveToLibrary = useCallback(async () => {
if (!featuredContent) return;
try {
if (isSaved) {
await catalogService.removeFromLibrary(featuredContent.type, featuredContent.id);
} else {
await catalogService.addToLibrary(featuredContent);
}
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch (error) {
console.error('Error updating library:', error);
}
}, [featuredContent, isSaved]);
const handleCategoryChange = (categoryId: string) => { const handleCategoryChange = (categoryId: string) => {
setSelectedCategory(categoryId); setSelectedCategory(categoryId);
}; };
@ -539,7 +585,9 @@ const HomeScreen = () => {
title: featuredContent.name, title: featuredContent.name,
year: featuredContent.year, year: featuredContent.year,
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
streamProvider: stream.name streamProvider: stream.name,
id: featuredContent.id,
type: featuredContent.type
}); });
}, [featuredContent, navigation]); }, [featuredContent, navigation]);
@ -547,14 +595,7 @@ const HomeScreen = () => {
if (!featuredContent) return null; if (!featuredContent) return null;
return ( return (
<TouchableOpacity <View style={styles.featuredContainer}>
style={styles.featuredContainer}
activeOpacity={0.9}
onPress={() => navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
})}
>
<ImageBackground <ImageBackground
source={{ uri: featuredContent.banner || featuredContent.poster }} source={{ uri: featuredContent.banner || featuredContent.poster }}
style={styles.featuredBanner} style={styles.featuredBanner}
@ -563,15 +604,15 @@ const HomeScreen = () => {
<LinearGradient <LinearGradient
colors={[ colors={[
'transparent', 'transparent',
`${colors.darkBackground}26`, 'rgba(0,0,0,0.2)',
`${colors.darkBackground}40`, 'rgba(0,0,0,0.6)',
`${colors.darkBackground}B3`, 'rgba(0,0,0,0.8)',
`${colors.darkBackground}E6`,
colors.darkBackground colors.darkBackground
]} ]}
locations={[0, 0.3, 0.5, 0.7, 0.85, 1]} locations={[0, 0.3, 0.6, 0.8, 1]}
style={styles.featuredGradient} style={styles.featuredGradient}
> >
<View style={{ flex: 1 }} />
<View style={styles.featuredContent}> <View style={styles.featuredContent}>
{featuredContent.logo ? ( {featuredContent.logo ? (
<ExpoImage <ExpoImage
@ -579,51 +620,39 @@ const HomeScreen = () => {
style={styles.featuredLogo} style={styles.featuredLogo}
contentFit="contain" contentFit="contain"
transition={200} transition={200}
cachePolicy="memory-disk"
/> />
) : ( ) : (
<Text style={styles.featuredTitle}>{featuredContent.name}</Text> <Text style={styles.featuredTitle}>{featuredContent.name}</Text>
)} )}
{featuredContent.genres && featuredContent.genres.length > 0 && ( <View style={styles.genreContainer}>
<View style={styles.genreContainer}> {featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
{featuredContent.genres.slice(0, 3).map((genre, index, array) => ( <React.Fragment key={index}>
<React.Fragment key={index}> <Text style={styles.genreText}>{genre}</Text>
<Text style={styles.genreText}>{genre}</Text> {index < array.length - 1 && (
{index < array.length - 1 && ( <Text style={styles.genreDot}></Text>
<Text style={styles.genreDot}></Text> )}
)} </React.Fragment>
</React.Fragment> ))}
))}
</View>
)}
<View style={styles.featuredMeta}>
{featuredContent.year && (
<View style={styles.yearChip}>
<Text style={styles.yearText}>{featuredContent.year}</Text>
</View>
)}
{featuredContent.imdbRating && (
<View style={styles.ratingContainer}>
<Image
source={{ uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/IMDB_Logo_2016.svg/575px-IMDB_Logo_2016.svg.png' }}
style={styles.imdbLogo}
/>
<Text style={styles.ratingText}>{featuredContent.imdbRating}</Text>
</View>
)}
</View> </View>
{featuredContent.description && (
<Text style={styles.description} numberOfLines={3}>
{featuredContent.description}
</Text>
)}
<View style={styles.featuredButtons}> <View style={styles.featuredButtons}>
<TouchableOpacity <TouchableOpacity
style={[styles.featuredButton, styles.playButton]} style={styles.myListButton}
onPress={handleSaveToLibrary}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={colors.white}
/>
<Text style={styles.myListButtonText}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.playButton}
onPress={() => { onPress={() => {
if (featuredContent) { if (featuredContent) {
navigation.navigate('Streams', { navigation.navigate('Streams', {
@ -633,25 +662,25 @@ const HomeScreen = () => {
} }
}} }}
> >
<MaterialIcons name="play-arrow" color="#000000" size={24} /> <MaterialIcons name="play-arrow" size={24} color={colors.black} />
<Text style={styles.playButtonText}>Play</Text> <Text style={styles.playButtonText}>Play</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.featuredButton, styles.infoButton]} style={styles.infoButton}
onPress={() => navigation.navigate('Metadata', { onPress={() => navigation.navigate('Metadata', {
id: featuredContent?.id, id: featuredContent?.id,
type: featuredContent?.type type: featuredContent?.type
})} })}
> >
<MaterialIcons name="info-outline" color="#FFFFFF" size={20} /> <MaterialIcons name="info-outline" size={24} color={colors.white} />
<Text style={styles.infoButtonText}>More Info</Text> <Text style={styles.infoButtonText}>Info</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</LinearGradient> </LinearGradient>
</ImageBackground> </ImageBackground>
</TouchableOpacity> </View>
); );
}; };
@ -705,7 +734,7 @@ const HomeScreen = () => {
if (loading && !refreshing) { if (loading && !refreshing) {
return ( return (
<SafeAreaView style={[styles.container]}> <View style={[styles.container]}>
<StatusBar <StatusBar
barStyle="light-content" barStyle="light-content"
backgroundColor="transparent" backgroundColor="transparent"
@ -720,18 +749,17 @@ const HomeScreen = () => {
<SkeletonCatalog key={index} /> <SkeletonCatalog key={index} />
))} ))}
</ScrollView> </ScrollView>
</SafeAreaView> </View>
); );
} }
return ( return (
<SafeAreaView style={[styles.container]}> <View style={[styles.container]}>
<StatusBar <StatusBar
barStyle="light-content" barStyle="light-content"
backgroundColor="transparent" backgroundColor="transparent"
translucent translucent
/> />
<ScrollView <ScrollView
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.text} /> <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.text} />
@ -761,14 +789,14 @@ const HomeScreen = () => {
</View> </View>
)} )}
</ScrollView> </ScrollView>
</SafeAreaView> </View>
); );
}; };
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const POSTER_WIDTH = (width - 40) / 2.7; const POSTER_WIDTH = (width - 40) / 2.7;
const styles = StyleSheet.create({ const styles = StyleSheet.create<any>({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
@ -783,17 +811,10 @@ const styles = StyleSheet.create({
}, },
featuredContainer: { featuredContainer: {
width: '100%', width: '100%',
height: height * 0.75, height: height * 0.65,
marginTop: -(StatusBar.currentHeight || 0), marginTop: 0,
marginBottom: 0, marginBottom: 0,
borderBottomLeftRadius: 0, position: 'relative',
borderBottomRightRadius: 0,
overflow: 'hidden',
elevation: 0,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
}, },
featuredBanner: { featuredBanner: {
width: '100%', width: '100%',
@ -802,131 +823,112 @@ const styles = StyleSheet.create({
featuredGradient: { featuredGradient: {
width: '100%', width: '100%',
height: '100%', height: '100%',
justifyContent: 'flex-end', justifyContent: 'space-between',
}, },
featuredContent: { featuredContent: {
padding: 24, padding: 24,
paddingBottom: 24, paddingBottom: 16,
alignItems: 'center',
flex: 1,
justifyContent: 'flex-end',
gap: 8,
}, },
featuredLogo: { featuredLogo: {
width: width * 0.6, width: width * 0.7,
height: 100, height: 100,
marginBottom: 0, marginBottom: 0,
alignSelf: 'flex-start', alignSelf: 'center',
backgroundColor: colors.transparent,
minHeight: 60,
maxHeight: 100,
}, },
featuredTitle: { featuredTitle: {
color: colors.highEmphasis, color: colors.white,
fontSize: 32, fontSize: 32,
fontWeight: '900', fontWeight: '900',
marginBottom: 16, marginBottom: 0,
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.5)', textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 2 }, textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 4, textShadowRadius: 4,
letterSpacing: -0.5,
},
description: {
color: colors.mediumEmphasis,
fontSize: 15,
lineHeight: 22,
marginBottom: 20,
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
}, },
genreContainer: { genreContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, justifyContent: 'center',
marginBottom: 0,
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 4,
}, },
genreText: { genreText: {
color: colors.highEmphasis, color: colors.white,
fontSize: 14, fontSize: 13,
fontWeight: '500', fontWeight: '500',
opacity: 0.8, opacity: 0.9,
}, },
genreDot: { genreDot: {
color: colors.highEmphasis, color: colors.white,
fontSize: 14,
marginHorizontal: 8,
opacity: 0.6,
},
yearChip: {
backgroundColor: colors.elevation3,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
marginRight: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
yearText: {
color: colors.highEmphasis,
fontSize: 12,
fontWeight: '600',
},
featuredMeta: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
marginTop: 4,
},
ratingContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.elevation3,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
ratingText: {
color: colors.highEmphasis,
marginLeft: 4,
fontWeight: '700',
fontSize: 13, fontSize: 13,
marginHorizontal: 4,
opacity: 0.6,
}, },
featuredButtons: { featuredButtons: {
flexDirection: 'row', flexDirection: 'row',
gap: 16, alignItems: 'flex-end',
marginTop: 12, justifyContent: 'space-evenly',
}, width: '100%',
featuredButton: {
flex: 1, flex: 1,
maxHeight: 60,
paddingTop: 12,
},
playButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14, paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 100, borderRadius: 100,
backgroundColor: colors.white,
elevation: 4, elevation: 4,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4, shadowRadius: 4,
flex: 0,
width: 150,
}, },
playButton: { myListButton: {
backgroundColor: colors.white, flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
gap: 6,
width: 40,
height: 41,
flex: null,
},
infoButton: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
gap: 4,
width: 40,
height: 39,
flex: null,
}, },
playButtonText: { playButtonText: {
color: colors.black, color: colors.black,
fontWeight: '700', fontWeight: '600',
marginLeft: 8, marginLeft: 8,
fontSize: 16, fontSize: 16,
}, },
infoButton: { myListButtonText: {
backgroundColor: colors.elevation2, color: colors.white,
borderWidth: 2, fontSize: 12,
borderColor: colors.white, fontWeight: '500',
}, },
infoButtonText: { infoButtonText: {
color: colors.white, color: colors.white,
marginLeft: 8, fontSize: 12,
fontWeight: '700', fontWeight: '500',
fontSize: 16,
}, },
catalogContainer: { catalogContainer: {
marginBottom: 24, marginBottom: 24,

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@ -14,7 +14,7 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRoute, useNavigation } from '@react-navigation/native'; import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
@ -24,6 +24,7 @@ import { CastSection } from '../components/metadata/CastSection';
import { SeriesContent } from '../components/metadata/SeriesContent'; import { SeriesContent } from '../components/metadata/SeriesContent';
import { MovieContent } from '../components/metadata/MovieContent'; import { MovieContent } from '../components/metadata/MovieContent';
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
import AgeBadge from '../components/metadata/AgeBadge';
import { RouteParams, Episode } from '../types/metadata'; import { RouteParams, Episode } from '../types/metadata';
import Animated, { import Animated, {
useAnimatedStyle, useAnimatedStyle,
@ -41,6 +42,7 @@ import { RouteProp } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { TMDBService } from '../services/tmdbService'; import { TMDBService } from '../services/tmdbService';
import { storageService } from '../services/storageService';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
@ -51,10 +53,13 @@ const springConfig = {
stiffness: 100 stiffness: 100
}; };
// Add debug log for storageService
console.log('[MetadataScreen] StorageService instance:', storageService);
const MetadataScreen = () => { const MetadataScreen = () => {
const route = useRoute<RouteProp<Record<string, RouteParams>, string>>(); const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { id, type } = route.params; const { id, type, episodeId } = route.params;
const { const {
metadata, metadata,
@ -88,23 +93,319 @@ const MetadataScreen = () => {
const heroHeight = useSharedValue(height * 0.75); const heroHeight = useSharedValue(height * 0.75);
const contentTranslateY = useSharedValue(50); const contentTranslateY = useSharedValue(50);
// Add state for watch progress
const [watchProgress, setWatchProgress] = useState<{
currentTime: number;
duration: number;
lastUpdated: number;
episodeId?: string;
} | null>(null);
// Debug log for route params
console.log('[MetadataScreen] Component mounted with route params:', { id, type, episodeId });
// Function to get episode details from episodeId
const getEpisodeDetails = useCallback((episodeId: string) => {
// Try to parse from format "seriesId:season:episode"
const parts = episodeId.split(':');
if (parts.length === 3) {
const [, seasonNum, episodeNum] = parts;
// Find episode in our local episodes array
const episode = episodes.find(
ep => ep.season_number === parseInt(seasonNum) &&
ep.episode_number === parseInt(episodeNum)
);
if (episode) {
return {
seasonNumber: seasonNum,
episodeNumber: episodeNum,
episodeName: episode.name
};
}
}
// If not found by season/episode, try stremioId
const episodeByStremioId = episodes.find(ep => ep.stremioId === episodeId);
if (episodeByStremioId) {
return {
seasonNumber: episodeByStremioId.season_number.toString(),
episodeNumber: episodeByStremioId.episode_number.toString(),
episodeName: episodeByStremioId.name
};
}
return null;
}, [episodes]);
const loadWatchProgress = useCallback(async () => {
try {
if (id && type) {
if (type === 'series') {
const allProgress = await storageService.getAllWatchProgress();
// Function to get episode number from episodeId
const getEpisodeNumber = (epId: string) => {
const parts = epId.split(':');
if (parts.length === 3) {
return {
season: parseInt(parts[1]),
episode: parseInt(parts[2])
};
}
return null;
};
// Get all episodes for this series with progress
const seriesProgresses = Object.entries(allProgress)
.filter(([key]) => key.includes(`${type}:${id}:`))
.map(([key, value]) => ({
episodeId: key.split(`${type}:${id}:`)[1],
progress: value
}))
.filter(({ episodeId, progress }) => {
const progressPercent = (progress.currentTime / progress.duration) * 100;
return progressPercent > 0;
});
// If we have a specific episodeId in route params
if (episodeId) {
const progress = await storageService.getWatchProgress(id, type, episodeId);
if (progress) {
const progressPercent = (progress.currentTime / progress.duration) * 100;
// If current episode is finished (≥95%), try to find next unwatched episode
if (progressPercent >= 95) {
const currentEpNum = getEpisodeNumber(episodeId);
if (currentEpNum && episodes.length > 0) {
// Find the next episode
const nextEpisode = episodes.find(ep => {
// First check in same season
if (ep.season_number === currentEpNum.season && ep.episode_number > currentEpNum.episode) {
const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
const epProgress = seriesProgresses.find(p => p.episodeId === epId);
if (!epProgress) return true;
const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100;
return percent < 95;
}
// Then check next seasons
if (ep.season_number > currentEpNum.season) {
const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
const epProgress = seriesProgresses.find(p => p.episodeId === epId);
if (!epProgress) return true;
const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100;
return percent < 95;
}
return false;
});
if (nextEpisode) {
const nextEpisodeId = nextEpisode.stremioId ||
`${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`;
const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId);
if (nextProgress) {
setWatchProgress({ ...nextProgress, episodeId: nextEpisodeId });
} else {
setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: nextEpisodeId });
}
return;
}
}
// If no next episode found or current episode is finished, show no progress
setWatchProgress(null);
return;
}
// If current episode is not finished, show its progress
setWatchProgress({ ...progress, episodeId });
} else {
setWatchProgress(null);
}
} else {
// Find the first unfinished episode
const unfinishedEpisode = episodes.find(ep => {
const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
const progress = seriesProgresses.find(p => p.episodeId === epId);
if (!progress) return true;
const percent = (progress.progress.currentTime / progress.progress.duration) * 100;
return percent < 95;
});
if (unfinishedEpisode) {
const epId = unfinishedEpisode.stremioId ||
`${id}:${unfinishedEpisode.season_number}:${unfinishedEpisode.episode_number}`;
const progress = await storageService.getWatchProgress(id, type, epId);
if (progress) {
setWatchProgress({ ...progress, episodeId: epId });
} else {
setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: epId });
}
} else {
setWatchProgress(null);
}
}
} else {
// For movies
const progress = await storageService.getWatchProgress(id, type, episodeId);
if (progress && progress.currentTime > 0) {
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 95) {
setWatchProgress(null);
} else {
setWatchProgress({ ...progress, episodeId });
}
} else {
setWatchProgress(null);
}
}
}
} catch (error) {
console.error('[MetadataScreen] Error loading watch progress:', error);
setWatchProgress(null);
}
}, [id, type, episodeId, episodes]);
// Initial load
useEffect(() => {
loadWatchProgress();
}, [loadWatchProgress]);
// Refresh when screen comes into focus
useFocusEffect(
useCallback(() => {
loadWatchProgress();
}, [loadWatchProgress])
);
// Function to get play button text
const getPlayButtonText = useCallback(() => {
if (!watchProgress || watchProgress.currentTime <= 0) {
return 'Play';
}
// Consider episode complete if progress is >= 95%
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
if (progressPercent >= 95) {
return 'Play';
}
return 'Resume';
}, [watchProgress]);
// Update the watch progress display
const renderWatchProgress = () => {
if (!watchProgress) {
return null;
}
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString();
let episodeInfo = '';
if (type === 'series' && watchProgress.episodeId) {
const details = getEpisodeDetails(watchProgress.episodeId);
if (details) {
episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
}
}
return (
<View style={styles.watchProgressContainer}>
<View style={styles.watchProgressBar}>
<View
style={[
styles.watchProgressFill,
{ width: `${progressPercent}%` }
]}
/>
</View>
<Text style={styles.watchProgressText}>
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} Last watched on {formattedTime}
</Text>
</View>
);
};
// Update the action buttons section
const ActionButtons = () => (
<View style={styles.actionButtons}>
<TouchableOpacity
style={[styles.actionButton, styles.playButton]}
onPress={handleShowStreams}
>
<MaterialIcons
name={watchProgress && watchProgress.currentTime > 0 ? "play-circle-outline" : "play-arrow"}
size={24}
color="#000"
/>
<Text style={styles.playButtonText}>
{getPlayButtonText()}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.infoButton]}
onPress={toggleLibrary}
>
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={24}
color="#fff"
/>
<Text style={styles.infoButtonText}>
{inLibrary ? 'Saved' : 'Save'}
</Text>
</TouchableOpacity>
{type === 'series' && (
<TouchableOpacity
style={[styles.iconButton]}
onPress={async () => {
const tmdb = TMDBService.getInstance();
const tmdbId = await tmdb.extractTMDBIdFromStremioId(id);
if (tmdbId) {
navigation.navigate('ShowRatings', { showId: tmdbId });
} else {
console.error('Could not find TMDB ID for show');
}
}}
>
<MaterialIcons name="star-rate" size={24} color="#fff" />
</TouchableOpacity>
)}
</View>
);
// Handler functions // Handler functions
const handleShowStreams = useCallback(() => { const handleShowStreams = useCallback(() => {
if (type === 'series' && episodes.length > 0) { if (type === 'series') {
const firstEpisode = episodes[0]; // If we have watch progress with an episodeId, use that
const episodeId = firstEpisode.stremioId || `${id}:${firstEpisode.season_number}:${firstEpisode.episode_number}`; if (watchProgress?.episodeId) {
navigation.navigate('Streams', { navigation.navigate('Streams', {
id, id,
type, type,
episodeId episodeId: watchProgress.episodeId
}); });
} else { return;
navigation.navigate('Streams', { }
id,
type // If we have a specific episodeId from route params, use that
}); if (episodeId) {
navigation.navigate('Streams', { id, type, episodeId });
return;
}
// Otherwise, if we have episodes, start with the first one
if (episodes.length > 0) {
const firstEpisode = episodes[0];
const newEpisodeId = firstEpisode.stremioId || `${id}:${firstEpisode.season_number}:${firstEpisode.episode_number}`;
navigation.navigate('Streams', { id, type, episodeId: newEpisodeId });
return;
}
} }
}, [navigation, id, type, episodes]);
navigation.navigate('Streams', { id, type, episodeId });
}, [navigation, id, type, episodes, episodeId, watchProgress]);
const handleSelectCastMember = (castMember: any) => { const handleSelectCastMember = (castMember: any) => {
// TODO: Implement cast member selection // TODO: Implement cast member selection
@ -380,6 +681,9 @@ const MetadataScreen = () => {
<Text style={styles.titleText}>{metadata.name}</Text> <Text style={styles.titleText}>{metadata.name}</Text>
)} )}
{/* Watch Progress */}
{renderWatchProgress()}
{/* Genre Tags */} {/* Genre Tags */}
{metadata.genres && metadata.genres.length > 0 && ( {metadata.genres && metadata.genres.length > 0 && (
<View style={styles.genreContainer}> <View style={styles.genreContainer}>
@ -395,47 +699,7 @@ const MetadataScreen = () => {
)} )}
{/* Action Buttons */} {/* Action Buttons */}
<View style={styles.actionButtons}> <ActionButtons />
<TouchableOpacity
style={[styles.actionButton, styles.playButton]}
onPress={handleShowStreams}
>
<MaterialIcons name="play-arrow" size={24} color="#000" />
<Text style={styles.playButtonText}>Play</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.infoButton]}
onPress={toggleLibrary}
>
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={24}
color="#fff"
/>
<Text style={styles.infoButtonText}>
{inLibrary ? 'Saved' : 'Save'}
</Text>
</TouchableOpacity>
{type === 'series' && (
<TouchableOpacity
style={[styles.iconButton]}
onPress={async () => {
const tmdb = TMDBService.getInstance();
const tmdbId = await tmdb.extractTMDBIdFromStremioId(id);
if (tmdbId) {
navigation.navigate('ShowRatings', { showId: tmdbId });
} else {
// TODO: Show error toast
console.error('Could not find TMDB ID for show');
}
}}
>
<MaterialIcons name="star-rate" size={24} color="#fff" />
</TouchableOpacity>
)}
</View>
</Animated.View> </Animated.View>
</LinearGradient> </LinearGradient>
</ImageBackground> </ImageBackground>
@ -446,14 +710,13 @@ const MetadataScreen = () => {
{/* Meta Info */} {/* Meta Info */}
<View style={styles.metaInfo}> <View style={styles.metaInfo}>
{metadata.year && ( {metadata.year && (
<View style={styles.metaChip}> <Text style={styles.metaText}>{metadata.year}</Text>
<Text style={styles.metaChipText}>{metadata.year}</Text>
</View>
)} )}
{metadata.runtime && ( {metadata.runtime && (
<View style={styles.metaChip}> <Text style={styles.metaText}>{metadata.runtime}</Text>
<Text style={styles.metaChipText}>{metadata.runtime}</Text> )}
</View> {metadata.certification && (
<AgeBadge rating={metadata.certification} />
)} )}
{metadata.imdbRating && ( {metadata.imdbRating && (
<View style={styles.ratingContainer}> <View style={styles.ratingContainer}>
@ -698,24 +961,17 @@ const styles = StyleSheet.create({
metaInfo: { metaInfo: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 16,
paddingHorizontal: 24,
marginBottom: 16, marginBottom: 16,
flexWrap: 'wrap',
paddingHorizontal: 16,
paddingTop: 16,
}, },
metaChip: { metaText: {
backgroundColor: colors.elevation3, color: colors.text,
paddingHorizontal: 12, fontSize: 15,
paddingVertical: 6, fontWeight: '700',
borderRadius: 16, letterSpacing: 0.3,
borderWidth: 1, textTransform: 'uppercase',
borderColor: 'rgba(255,255,255,0.1)', opacity: 0.9,
},
metaChipText: {
color: colors.highEmphasis,
fontSize: 12,
fontWeight: '600',
}, },
ratingContainer: { ratingContainer: {
flexDirection: 'row', flexDirection: 'row',
@ -723,7 +979,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.elevation3, backgroundColor: colors.elevation3,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 12, borderRadius: 4,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)', borderColor: 'rgba(255,255,255,0.1)',
}, },
@ -869,6 +1125,30 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
flex: 1, flex: 1,
}, },
watchProgressContainer: {
marginTop: 8,
marginBottom: 16,
width: '100%',
alignItems: 'center',
},
watchProgressBar: {
width: '80%',
height: 3,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 1.5,
overflow: 'hidden',
marginBottom: 8,
},
watchProgressFill: {
height: '100%',
backgroundColor: colors.primary,
borderRadius: 1.5,
},
watchProgressText: {
color: colors.textMuted,
fontSize: 12,
textAlign: 'center',
},
}); });
export default MetadataScreen; export default MetadataScreen;

View file

@ -12,7 +12,7 @@ import {
StatusBar, StatusBar,
Keyboard, Keyboard,
Dimensions, Dimensions,
SectionList, ScrollView,
Animated as RNAnimated, Animated as RNAnimated,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
@ -27,6 +27,8 @@ import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5;
const POSTER_WIDTH = 90; const POSTER_WIDTH = 90;
const POSTER_HEIGHT = 135; const POSTER_HEIGHT = 135;
const RECENT_SEARCHES_KEY = 'recent_searches'; const RECENT_SEARCHES_KEY = 'recent_searches';
@ -62,11 +64,11 @@ const SkeletonLoader = () => {
}); });
const renderSkeletonItem = () => ( const renderSkeletonItem = () => (
<View style={styles.resultItem}> <View style={styles.skeletonVerticalItem}>
<RNAnimated.View style={[styles.skeletonPoster, { opacity }]} /> <RNAnimated.View style={[styles.skeletonPoster, { opacity }]} />
<View style={styles.itemDetails}> <View style={styles.skeletonItemDetails}>
<RNAnimated.View style={[styles.skeletonTitle, { opacity }]} /> <RNAnimated.View style={[styles.skeletonTitle, { opacity }]} />
<View style={styles.metaRow}> <View style={styles.skeletonMetaRow}>
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} /> <RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} /> <RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
</View> </View>
@ -88,28 +90,22 @@ const SkeletonLoader = () => {
); );
}; };
type SearchFilter = 'all' | 'movie' | 'series';
const SearchScreen = () => { const SearchScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
// Always use dark mode
const isDarkMode = true; const isDarkMode = true;
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [results, setResults] = useState<StreamingContent[]>([]); const [results, setResults] = useState<StreamingContent[]>([]);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [searched, setSearched] = useState(false); const [searched, setSearched] = useState(false);
const [activeFilter, setActiveFilter] = useState<SearchFilter>('all');
const [recentSearches, setRecentSearches] = useState<string[]>([]); const [recentSearches, setRecentSearches] = useState<string[]>([]);
const [showRecent, setShowRecent] = useState(true); const [showRecent, setShowRecent] = useState(true);
// Set navigation options to hide the header
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerShown: false, headerShown: false,
}); });
}, [navigation]); }, [navigation]);
// Load recent searches on mount
useEffect(() => { useEffect(() => {
loadRecentSearches(); loadRecentSearches();
}, []); }, []);
@ -139,28 +135,9 @@ const SearchScreen = () => {
} }
}; };
// Fuzzy search implementation
const fuzzyMatch = (str: string, pattern: string): boolean => {
pattern = pattern.toLowerCase();
str = str.toLowerCase();
let patternIdx = 0;
let strIdx = 0;
while (patternIdx < pattern.length && strIdx < str.length) {
if (pattern[patternIdx] === str[strIdx]) {
patternIdx++;
}
strIdx++;
}
return patternIdx === pattern.length;
};
// Debounced search function with fuzzy search
const debouncedSearch = useCallback( const debouncedSearch = useCallback(
debounce(async (searchQuery: string) => { debounce(async (searchQuery: string) => {
if (searchQuery.trim().length < 2) { if (!searchQuery.trim()) {
setResults([]); setResults([]);
setSearching(false); setSearching(false);
return; return;
@ -168,15 +145,7 @@ const SearchScreen = () => {
try { try {
const searchResults = await catalogService.searchContentCinemeta(searchQuery); const searchResults = await catalogService.searchContentCinemeta(searchQuery);
setResults(searchResults);
// Apply fuzzy search on the results
const fuzzyResults = searchResults.filter(item =>
fuzzyMatch(item.name, searchQuery) ||
(item.genres && item.genres.some(genre => fuzzyMatch(genre, searchQuery))) ||
(item.year && fuzzyMatch(item.year.toString(), searchQuery))
);
setResults(fuzzyResults);
await saveRecentSearch(searchQuery); await saveRecentSearch(searchQuery);
} catch (error) { } catch (error) {
console.error('Search failed:', error); console.error('Search failed:', error);
@ -184,12 +153,12 @@ const SearchScreen = () => {
} finally { } finally {
setSearching(false); setSearching(false);
} }
}, 200), // Reduced from 300ms to 200ms for better responsiveness }, 200),
[] [recentSearches]
); );
useEffect(() => { useEffect(() => {
if (query.trim().length >= 2) { if (query.trim()) {
setSearching(true); setSearching(true);
setSearched(true); setSearched(true);
setShowRecent(false); setShowRecent(false);
@ -198,55 +167,16 @@ const SearchScreen = () => {
setResults([]); setResults([]);
setSearched(false); setSearched(false);
setShowRecent(true); setShowRecent(true);
loadRecentSearches();
} }
}, [query, debouncedSearch]); }, [query]);
const handleClearSearch = () => { const handleClearSearch = () => {
setQuery(''); setQuery('');
setResults([]); setResults([]);
setSearched(false); setSearched(false);
setActiveFilter('all');
setShowRecent(true); setShowRecent(true);
}; loadRecentSearches();
const renderSearchFilters = () => {
const filters: { id: SearchFilter; label: string; icon: keyof typeof MaterialIcons.glyphMap }[] = [
{ id: 'all', label: 'All', icon: 'apps' },
{ id: 'movie', label: 'Movies', icon: 'movie' },
{ id: 'series', label: 'TV Shows', icon: 'tv' },
];
return (
<View style={styles.filtersContainer}>
{filters.map((filter) => (
<TouchableOpacity
key={filter.id}
style={[
styles.filterButton,
activeFilter === filter.id && styles.filterButtonActive,
{ borderColor: isDarkMode ? 'rgba(255,255,255,0.1)' : colors.border }
]}
onPress={() => setActiveFilter(filter.id)}
>
<MaterialIcons
name={filter.icon}
size={20}
color={activeFilter === filter.id ? colors.primary : (isDarkMode ? colors.lightGray : colors.mediumGray)}
style={styles.filterIcon}
/>
<Text
style={[
styles.filterText,
activeFilter === filter.id && styles.filterTextActive,
{ color: isDarkMode ? colors.white : colors.black }
]}
>
{filter.label}
</Text>
</TouchableOpacity>
))}
</View>
);
}; };
const renderRecentSearches = () => { const renderRecentSearches = () => {
@ -254,14 +184,17 @@ const SearchScreen = () => {
return ( return (
<View style={styles.recentSearchesContainer}> <View style={styles.recentSearchesContainer}>
<Text style={[styles.recentSearchesTitle, { color: isDarkMode ? colors.white : colors.black }]}> <Text style={[styles.carouselTitle, { color: isDarkMode ? colors.white : colors.black }]}>
Recent Searches Recent Searches
</Text> </Text>
{recentSearches.map((search, index) => ( {recentSearches.map((search, index) => (
<TouchableOpacity <TouchableOpacity
key={index} key={index}
style={styles.recentSearchItem} style={styles.recentSearchItem}
onPress={() => setQuery(search)} onPress={() => {
setQuery(search);
Keyboard.dismiss();
}}
> >
<MaterialIcons <MaterialIcons
name="history" name="history"
@ -281,90 +214,43 @@ const SearchScreen = () => {
); );
}; };
const renderItem = ({ item, index, section }: { item: StreamingContent; index: number; section: { title: string; data: StreamingContent[] } }) => { const renderHorizontalItem = ({ item }: { item: StreamingContent }) => {
return ( return (
<TouchableOpacity <TouchableOpacity
style={[ style={styles.horizontalItem}
styles.resultItem,
{ backgroundColor: isDarkMode ? colors.darkBackground : colors.white }
]}
onPress={() => { onPress={() => {
navigation.navigate('Metadata', { id: item.id, type: item.type }); navigation.navigate('Metadata', { id: item.id, type: item.type });
}} }}
> >
<View style={styles.posterContainer}> <View style={styles.horizontalItemPosterContainer}>
<Image <Image
source={{ uri: item.poster || PLACEHOLDER_POSTER }} source={{ uri: item.poster || PLACEHOLDER_POSTER }}
style={styles.poster} style={styles.horizontalItemPoster}
contentFit="cover" contentFit="cover"
transition={300}
/> />
</View> </View>
<Text
<View style={styles.itemDetails}> style={[styles.horizontalItemTitle, { color: isDarkMode ? colors.white : colors.black }]}
<Text numberOfLines={2}
style={[styles.itemTitle, { color: isDarkMode ? colors.white : colors.black }]} >
numberOfLines={2} {item.name}
> </Text>
{item.name}
</Text>
<View style={styles.metaRow}>
{item.year && (
<Text style={[styles.yearText, { color: isDarkMode ? colors.lightGray : colors.mediumGray }]}>
{item.year}
</Text>
)}
{item.genres && item.genres.length > 0 && (
<Text style={[styles.genreText, { color: isDarkMode ? colors.lightGray : colors.mediumGray }]}>
{item.genres[0]}
</Text>
)}
</View>
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
const movieResults = useMemo(() => {
return results.filter(item => item.type === 'movie');
}, [results]);
const renderSectionHeader = ({ section: { title, data } }: { section: { title: string; data: StreamingContent[] } }) => ( const seriesResults = useMemo(() => {
<View style={[ return results.filter(item => item.type === 'series');
styles.sectionHeader, }, [results]);
{ backgroundColor: isDarkMode ? colors.darkBackground : colors.lightBackground }
]}>
<Text style={[styles.sectionTitle, { color: isDarkMode ? colors.white : colors.black }]}>
{title} ({data.length})
</Text>
</View>
);
// Categorize results const hasResultsToShow = useMemo(() => {
const categorizedResults = useMemo(() => { return movieResults.length > 0 || seriesResults.length > 0;
if (!results.length) return []; }, [movieResults, seriesResults]);
const movieResults = results.filter(item => item.type === 'movie');
const seriesResults = results.filter(item => item.type === 'series');
const sections = [];
if (activeFilter === 'all' || activeFilter === 'movie') {
if (movieResults.length > 0) {
sections.push({
title: 'Movies',
data: movieResults,
});
}
}
if (activeFilter === 'all' || activeFilter === 'series') {
if (seriesResults.length > 0) {
sections.push({
title: 'TV Shows',
data: seriesResults,
});
}
}
return sections;
}, [results, activeFilter]);
return ( return (
<SafeAreaView style={[ <SafeAreaView style={[
@ -420,11 +306,9 @@ const SearchScreen = () => {
</View> </View>
</View> </View>
{renderSearchFilters()}
{searching ? ( {searching ? (
<SkeletonLoader /> <SkeletonLoader />
) : searched && categorizedResults.length === 0 ? ( ) : searched && !hasResultsToShow ? (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<MaterialIcons <MaterialIcons
name="search-off" name="search-off"
@ -445,19 +329,43 @@ const SearchScreen = () => {
</Text> </Text>
</View> </View>
) : ( ) : (
<> <ScrollView
{renderRecentSearches()} style={styles.scrollView}
<SectionList contentContainerStyle={styles.scrollViewContent}
sections={categorizedResults} keyboardShouldPersistTaps="handled"
renderItem={renderItem} onScrollBeginDrag={Keyboard.dismiss}
renderSectionHeader={renderSectionHeader} >
keyExtractor={item => `${item.type}-${item.id}`} {!query.trim() && renderRecentSearches()}
contentContainerStyle={styles.resultsList}
showsVerticalScrollIndicator={false} {movieResults.length > 0 && (
keyboardShouldPersistTaps="handled" <View style={styles.carouselContainer}>
stickySectionHeadersEnabled={true} <Text style={styles.carouselTitle}>Movies ({movieResults.length})</Text>
/> <FlatList
</> data={movieResults}
renderItem={renderHorizontalItem}
keyExtractor={item => `movie-${item.id}`}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalListContent}
/>
</View>
)}
{seriesResults.length > 0 && (
<View style={styles.carouselContainer}>
<Text style={styles.carouselTitle}>TV Shows ({seriesResults.length})</Text>
<FlatList
data={seriesResults}
renderItem={renderHorizontalItem}
keyExtractor={item => `series-${item.id}`}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalListContent}
/>
</View>
)}
</ScrollView>
)} )}
</SafeAreaView> </SafeAreaView>
); );
@ -498,95 +406,64 @@ const styles = StyleSheet.create({
clearButton: { clearButton: {
padding: 4, padding: 4,
}, },
filtersContainer: { scrollView: {
flexDirection: 'row', flex: 1,
paddingHorizontal: 16,
paddingVertical: 16,
gap: 8,
}, },
filterButton: { scrollViewContent: {
flexDirection: 'row', paddingBottom: 20,
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.darkGray,
backgroundColor: 'transparent',
gap: 6,
}, },
filterButtonActive: { carouselContainer: {
backgroundColor: colors.primary + '20', marginBottom: 24,
borderColor: colors.primary,
}, },
filterIcon: { carouselTitle: {
marginRight: 2,
},
filterText: {
fontSize: 14,
fontWeight: '500',
},
filterTextActive: {
color: colors.primary,
fontWeight: '600',
},
sectionHeader: {
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
sectionTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: colors.white,
marginBottom: 12,
paddingHorizontal: 16,
}, },
resultsList: { horizontalListContent: {
padding: 16, paddingHorizontal: 16,
paddingRight: 8,
}, },
resultItem: { horizontalItem: {
flexDirection: 'row', width: HORIZONTAL_ITEM_WIDTH,
marginBottom: 16, marginRight: 12,
borderRadius: 12,
padding: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}, },
posterContainer: { horizontalItemPosterContainer: {
width: POSTER_WIDTH, width: HORIZONTAL_ITEM_WIDTH,
height: POSTER_HEIGHT, height: HORIZONTAL_POSTER_HEIGHT,
borderRadius: 8, borderRadius: 8,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
marginBottom: 8,
}, },
poster: { horizontalItemPoster: {
width: '100%', width: '100%',
height: '100%', height: '100%',
}, },
itemDetails: { horizontalItemTitle: {
flex: 1, fontSize: 14,
marginLeft: 16, fontWeight: '500',
justifyContent: 'center', lineHeight: 18,
textAlign: 'left',
}, },
itemTitle: { recentSearchesContainer: {
fontSize: 16, paddingHorizontal: 0,
fontWeight: 'bold', paddingBottom: 16,
marginBottom: 8,
lineHeight: 22,
}, },
metaRow: { recentSearchItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', paddingVertical: 10,
gap: 8, paddingHorizontal: 16,
}, },
yearText: { recentSearchIcon: {
fontSize: 14, marginRight: 12,
}, },
genreText: { recentSearchText: {
fontSize: 14, fontSize: 16,
flex: 1,
}, },
loadingContainer: { loadingContainer: {
flex: 1, flex: 1,
@ -617,48 +494,45 @@ const styles = StyleSheet.create({
skeletonContainer: { skeletonContainer: {
padding: 16, padding: 16,
}, },
skeletonVerticalItem: {
flexDirection: 'row',
marginBottom: 16,
},
skeletonPoster: { skeletonPoster: {
width: POSTER_WIDTH, width: POSTER_WIDTH,
height: POSTER_HEIGHT, height: POSTER_HEIGHT,
borderRadius: 8, borderRadius: 8,
overflow: 'hidden',
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
}, },
skeletonItemDetails: {
flex: 1,
marginLeft: 16,
justifyContent: 'center',
},
skeletonMetaRow: {
flexDirection: 'row',
gap: 8,
marginTop: 8,
},
skeletonTitle: { skeletonTitle: {
height: 22, height: 20,
width: '80%',
marginBottom: 8, marginBottom: 8,
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
borderRadius: 4,
}, },
skeletonMeta: { skeletonMeta: {
height: 14, height: 14,
width: '30%',
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
borderRadius: 4,
}, },
skeletonSectionHeader: { skeletonSectionHeader: {
paddingHorizontal: 16, height: 24,
paddingVertical: 12, width: '40%',
borderBottomWidth: 1, backgroundColor: colors.darkBackground,
borderBottomColor: 'rgba(255,255,255,0.1)', marginBottom: 16,
}, borderRadius: 4,
recentSearchesContainer: {
padding: 16,
},
recentSearchesTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
},
recentSearchItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
recentSearchIcon: {
marginRight: 12,
},
recentSearchText: {
fontSize: 16,
}, },
}); });

View file

@ -27,6 +27,7 @@ import { tmdbService } from '../services/tmdbService';
import { stremioService } from '../services/stremioService'; import { stremioService } from '../services/stremioService';
import { VideoPlayerService } from '../services/videoPlayerService'; import { VideoPlayerService } from '../services/videoPlayerService';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import QualityBadge from '../components/metadata/QualityBadge';
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeInDown, FadeInDown,
@ -136,22 +137,12 @@ const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, s
</View> </View>
<View style={styles.streamMetaRow}> <View style={styles.streamMetaRow}>
{quality && ( {quality && quality >= "720" && (
<View style={[styles.chip, { backgroundColor: colors.info }]}> <QualityBadge type="HD" />
<Text style={styles.chipText}>{quality}p</Text>
</View>
)}
{isHDR && (
<View style={[styles.chip, { backgroundColor: colors.success }]}>
<Text style={styles.chipText}>HDR</Text>
</View>
)} )}
{isDolby && ( {isDolby && (
<View style={[styles.chip, { backgroundColor: colors.warning }]}> <QualityBadge type="VISION" />
<Text style={styles.chipText}>DOLBY</Text>
</View>
)} )}
{size && ( {size && (
@ -583,7 +574,10 @@ export const StreamsScreen = () => {
episode: type === 'series' ? currentEpisode?.episode_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined,
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
year: metadata?.year, year: metadata?.year,
streamProvider: stream.name streamProvider: stream.name,
id,
type,
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
}); });
} }
} else { } else {
@ -596,7 +590,10 @@ export const StreamsScreen = () => {
episode: type === 'series' ? currentEpisode?.episode_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined,
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
year: metadata?.year, year: metadata?.year,
streamProvider: stream.name streamProvider: stream.name,
id,
type,
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
}); });
} }
} }
@ -715,7 +712,10 @@ export const StreamsScreen = () => {
episodeTitle: type === 'series' ? currentEpisode?.name : undefined, episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
season: type === 'series' ? currentEpisode?.season_number : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined,
episode: type === 'series' ? currentEpisode?.episode_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined,
year: metadata?.year year: metadata?.year,
id,
type,
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
}); });
} }
} else { } else {
@ -726,7 +726,10 @@ export const StreamsScreen = () => {
episodeTitle: type === 'series' ? currentEpisode?.name : undefined, episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
season: type === 'series' ? currentEpisode?.season_number : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined,
episode: type === 'series' ? currentEpisode?.episode_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined,
year: metadata?.year year: metadata?.year,
id,
type,
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
}); });
} }

View file

@ -2,12 +2,9 @@ import React, { useState, useRef, useEffect } from 'react';
import { View, TouchableOpacity, StyleSheet, Text, Dimensions, Modal, Pressable, StatusBar, Platform, ScrollView, Animated, StyleProp, ViewStyle } from 'react-native'; import { View, TouchableOpacity, StyleSheet, Text, Dimensions, Modal, Pressable, StatusBar, Platform, ScrollView, Animated, StyleProp, ViewStyle } from 'react-native';
import Video from 'react-native-video'; import Video from 'react-native-video';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Slider } from 'react-native-awesome-slider'; import Slider from '@react-native-community/slider';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useSharedValue, runOnJS } from 'react-native-reanimated'; // Remove reanimated import since we're not using shared values anymore
// Remove Gesture Handler imports
// import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler';
// Import for navigation bar hiding
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
// Import immersive mode package // Import immersive mode package
import RNImmersiveMode from 'react-native-immersive-mode'; import RNImmersiveMode from 'react-native-immersive-mode';
@ -16,6 +13,9 @@ import * as ScreenOrientation from 'expo-screen-orientation';
// Import navigation hooks // Import navigation hooks
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { storageService } from '../services/storageService';
// Add throttle/debounce imports
import { debounce } from 'lodash';
// Define the TrackPreferenceType for audio/text tracks // Define the TrackPreferenceType for audio/text tracks
type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index'; type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index';
@ -35,6 +35,9 @@ interface VideoPlayerProps {
quality?: string; quality?: string;
year?: number; year?: number;
streamProvider?: string; streamProvider?: string;
id?: string; // Add id for the content
type?: string; // Add type (movie/series)
episodeId?: string; // Add episodeId for series episodes
} }
// Match the react-native-video AudioTrack type // Match the react-native-video AudioTrack type
@ -73,7 +76,10 @@ const VideoPlayer = () => {
episodeTitle, episodeTitle,
quality, quality,
year, year,
streamProvider streamProvider,
id, // Extract id
type, // Extract type
episodeId // Extract episodeId
} = route.params; } = route.params;
// Provide a fallback test URL in development mode if URI is empty or invalid // Provide a fallback test URL in development mode if URI is empty or invalid
@ -89,7 +95,10 @@ const VideoPlayer = () => {
episodeTitle, episodeTitle,
quality, quality,
year, year,
streamProvider streamProvider,
id,
type,
episodeId
}); });
// Validate URI // Validate URI
@ -122,9 +131,7 @@ const VideoPlayer = () => {
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain'); // State for resize mode const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain'); // State for resize mode
const [showAspectRatioMenu, setShowAspectRatioMenu] = useState(false); // New state for aspect ratio menu const [showAspectRatioMenu, setShowAspectRatioMenu] = useState(false); // New state for aspect ratio menu
const videoRef = useRef<any>(null); const videoRef = useRef<any>(null);
const progress = useSharedValue(0); const [sliderValue, setSliderValue] = useState(0);
const min = useSharedValue(0);
const max = useSharedValue(duration);
// Add state for the direct UI menus // Add state for the direct UI menus
const [showAudioOptions, setShowAudioOptions] = useState(false); const [showAudioOptions, setShowAudioOptions] = useState(false);
@ -136,6 +143,80 @@ const VideoPlayer = () => {
// Add a new state variable to track buffering progress // Add a new state variable to track buffering progress
const [bufferedProgress, setBufferedProgress] = useState(0); const [bufferedProgress, setBufferedProgress] = useState(0);
// Add state for tracking if initial seek is done
const [initialSeekDone, setInitialSeekDone] = useState(false);
const lastProgressUpdate = useRef<number>(0);
const PROGRESS_UPDATE_INTERVAL = 5000; // Update every 5 seconds
// Add ref for tracking if slider is being dragged
const isSliderDragging = useRef(false);
// Add last onProgress update time to throttle updates
const lastProgressUpdateTime = useRef(0);
// Load initial progress when component mounts
useEffect(() => {
const loadProgress = async () => {
if (id && type) {
const savedProgress = await storageService.getWatchProgress(id, type, episodeId);
if (savedProgress && savedProgress.currentTime > 0) {
// Only seek if we're not too close to the end
const threshold = savedProgress.duration * 0.9; // 90% threshold
if (savedProgress.currentTime < threshold) {
setCurrentTime(savedProgress.currentTime);
if (videoRef.current) {
videoRef.current.seek(savedProgress.currentTime);
}
}
}
}
};
loadProgress();
}, [id, type, episodeId]);
// Save progress periodically and when component unmounts
useEffect(() => {
const saveProgress = async () => {
if (id && type && duration > 0) {
const now = Date.now();
// Only update if enough time has passed since last update
if (now - lastProgressUpdate.current >= PROGRESS_UPDATE_INTERVAL) {
const progressPercent = (currentTime / duration) * 100;
// If progress is >= 95%, consider it complete and remove progress
if (progressPercent >= 95) {
await storageService.removeWatchProgress(id, type, episodeId);
} else {
await storageService.setWatchProgress(id, type, {
currentTime,
duration,
lastUpdated: now
}, episodeId);
}
lastProgressUpdate.current = now;
}
}
};
// Save progress periodically
const progressInterval = setInterval(saveProgress, PROGRESS_UPDATE_INTERVAL);
// Save progress when component unmounts
return () => {
clearInterval(progressInterval);
saveProgress();
};
}, [id, type, episodeId, currentTime, duration]);
// Handle video completion
const onEnd = async () => {
if (id && type) {
// Remove progress when video is finished
await storageService.removeWatchProgress(id, type, episodeId);
}
};
// Update the component mount effect to start auto-hide timer // Update the component mount effect to start auto-hide timer
useEffect(() => { useEffect(() => {
// Enable immersive mode when component mounts // Enable immersive mode when component mounts
@ -209,8 +290,10 @@ const VideoPlayer = () => {
}; };
useEffect(() => { useEffect(() => {
max.value = duration; if (duration > 0 && currentTime > 0) {
}, [duration]); setSliderValue(currentTime);
}
}, [duration, currentTime]);
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
if (isNaN(seconds)) return '0:00'; if (isNaN(seconds)) return '0:00';
@ -295,23 +378,49 @@ const VideoPlayer = () => {
// Add dependencies that should reset the timer // Add dependencies that should reset the timer
}, [currentTime]); }, [currentTime]);
const onSliderValueChange = (value: number) => { // Create a debounced version of the seek function to prevent excessive seeking
if (videoRef.current) { const debouncedSeek = useRef(
const newTime = Math.floor(value); debounce((time: number) => {
if (videoRef.current) {
// If seeking forward, preload the buffer at the new position videoRef.current.seek(time);
const isFastForward = newTime > currentTime;
if (isFastForward) {
// Reset buffered progress when seeking forward
setBufferedProgress(newTime);
} }
}, 50)
videoRef.current.seek(newTime); ).current;
setCurrentTime(newTime);
progress.value = newTime; // Modify slider value change handler for community slider
startHideControlsTimer(); const onSliderValueChange = (value: number) => {
setSliderValue(value);
setCurrentTime(value);
};
const onSlidingComplete = (value: number) => {
if (!videoRef.current) return;
const newTime = Math.floor(value);
// Update UI immediately for responsive feel
setCurrentTime(newTime);
setSliderValue(newTime);
// Seek to the new position
videoRef.current.seek(newTime);
// Reset buffered progress indicator if seeking forward
const isFastForward = newTime > currentTime;
if (isFastForward) {
setBufferedProgress(newTime);
} }
// Reset timer for auto-hiding controls
startHideControlsTimer();
// Reset slider dragging state
isSliderDragging.current = false;
};
// Set slider being touched
const onSlidingStart = () => {
isSliderDragging.current = true;
}; };
const togglePlayback = () => { const togglePlayback = () => {
@ -324,39 +433,41 @@ const VideoPlayer = () => {
const newTime = Math.max(0, Math.min(currentTime + seconds, duration)); const newTime = Math.max(0, Math.min(currentTime + seconds, duration));
videoRef.current.seek(newTime); videoRef.current.seek(newTime);
setCurrentTime(newTime); setCurrentTime(newTime);
progress.value = newTime; setSliderValue(newTime);
startHideControlsTimer(); startHideControlsTimer();
} }
}; };
// Update the onProgress handler to better manage buffering // Optimize the onProgress handler to throttle updates
const onProgress = (data: { currentTime: number, playableDuration?: number, seekableDuration?: number }) => { const onProgress = (data: { currentTime: number, playableDuration?: number, seekableDuration?: number }) => {
const newTime = Math.floor(data.currentTime); // Skip updates during dragging for smoother UX
// Only update when the second changes to avoid excessive state updates if (isSliderDragging.current) return;
if (Math.floor(currentTime) !== newTime) {
setCurrentTime(data.currentTime); // Throttle updates to reduce render cycles (process at most every 250ms)
progress.value = data.currentTime; const now = Date.now();
if (now - lastProgressUpdateTime.current < 250) {
return;
} }
// Update buffered progress with the maximum available duration lastProgressUpdateTime.current = now;
const newTime = data.currentTime;
// Only update when there's a meaningful change (at least 0.5 second difference)
if (Math.abs(currentTime - newTime) >= 0.5) {
setCurrentTime(newTime);
setSliderValue(newTime);
}
// Update buffered progress more efficiently
if (data.playableDuration) { if (data.playableDuration) {
// Use the maximum of the current buffer and the new buffer to prevent "shrinking" when seeking // Use a functional update to ensure we're working with the latest state
setBufferedProgress(prev => Math.max(prev, data.playableDuration || 0)); setBufferedProgress(prev => Math.max(prev, data.playableDuration || 0));
} }
// Log buffering stats in development mode
if (__DEV__ && data.seekableDuration) {
console.log(
`Buffering stats - Current: ${formatTime(data.currentTime)}, ` +
`Buffered: ${formatTime(data.playableDuration || 0)}, ` +
`Seekable: ${formatTime(data.seekableDuration)}`
);
}
}; };
const onLoad = (data: { duration: number }) => { const onLoad = (data: { duration: number }) => {
setDuration(data.duration); setDuration(data.duration);
max.value = data.duration;
}; };
const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => { const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => {
@ -542,8 +653,9 @@ const VideoPlayer = () => {
resizeMode={resizeMode} resizeMode={resizeMode}
onLoad={onLoad} onLoad={onLoad}
onProgress={onProgress} onProgress={onProgress}
onEnd={onEnd}
rate={playbackSpeed} rate={playbackSpeed}
progressUpdateInterval={100} // More frequent updates for better progress tracking progressUpdateInterval={500} // Less frequent updates (500ms vs 100ms)
selectedAudioTrack={selectedAudioTrack !== null ? selectedAudioTrack={selectedAudioTrack !== null ?
{ type: 'index', value: selectedAudioTrack } as any : { type: 'index', value: selectedAudioTrack } as any :
undefined undefined
@ -552,7 +664,7 @@ const VideoPlayer = () => {
selectedTextTrack={selectedTextTrack as any} selectedTextTrack={selectedTextTrack as any}
onTextTracks={onTextTracks} onTextTracks={onTextTracks}
// Add caching functionality // Optimize buffer configuration for smoother playback
bufferConfig={{ bufferConfig={{
minBufferMs: 15000, // 15 seconds minimum buffer minBufferMs: 15000, // 15 seconds minimum buffer
maxBufferMs: 50000, // 50 seconds maximum buffer maxBufferMs: 50000, // 50 seconds maximum buffer
@ -561,18 +673,21 @@ const VideoPlayer = () => {
}} }}
repeat={false} // Don't loop the video repeat={false} // Don't loop the video
// Add cache control // Performance optimization settings
ignoreSilentSwitch="ignore" // Keep playing when the app is in the background ignoreSilentSwitch="ignore" // Keep playing when the app is in the background
playInBackground={false} // Don't play when app is in background playInBackground={false} // Don't play when app is in background
reportBandwidth={false} // Disable bandwidth reporting to reduce overhead
disableFocus={false} // Stay focused disableFocus={false} // Stay focused
// Only render when actively used to save resources
renderToHardwareTextureAndroid={true}
onBuffer={(buffer) => { onBuffer={(buffer) => {
console.log('Buffering:', buffer.isBuffering); console.log('Buffering:', buffer.isBuffering);
}} }}
onError={(error) => { onError={(error) => {
console.error('Video playback error:', error); console.error('Video playback error:', error);
// Display error details for debugging
alert(`Video Error: ${error.error.errorString} (Code: ${error.error.errorCode})`); alert(`Video Error: ${error.error.errorString} (Code: ${error.error.errorCode})`);
}} }}
/> />
@ -637,35 +752,42 @@ const VideoPlayer = () => {
style={styles.bottomGradient} style={styles.bottomGradient}
> >
<View style={styles.bottomControls}> <View style={styles.bottomControls}>
{/* Slider */} {/* Slider - replaced with community slider */}
<View style={styles.sliderContainer}> <View style={styles.sliderContainer}>
{/* Buffer indicator */} {/* Buffer indicator - only render if needed */}
{bufferedProgress > 0 && ( {bufferedProgress > 0 && (
<View style={[ <View style={[
styles.bufferIndicator, styles.bufferIndicator,
{ {
width: `${(bufferedProgress / duration) * 100}%`, width: `${Math.min((bufferedProgress / (duration || 1)) * 100, 100)}%`
maxWidth: '100%'
} }
]} ]}
/> />
)} )}
<Slider
progress={progress} <View style={styles.sliderRow}>
minimumValue={min} <Text style={styles.currentTime}>
maximumValue={max} {formatTime(currentTime)}
style={styles.slider} </Text>
onValueChange={onSliderValueChange}
theme={{ <Slider
minimumTrackTintColor: '#E50914', style={styles.slider}
maximumTrackTintColor: 'rgba(255, 255, 255, 0.3)', value={sliderValue}
bubbleBackgroundColor: '#E50914', minimumValue={0}
cacheTrackTintColor: 'rgba(229, 9, 20, 0.5)', maximumValue={duration > 0 ? duration : 1}
}} step={0.1}
/> minimumTrackTintColor="#E50914"
<Text style={styles.duration}> maximumTrackTintColor="rgba(255, 255, 255, 0.3)"
{formatTime(currentTime)} / {formatTime(duration)} thumbTintColor="#E50914"
</Text> onValueChange={onSliderValueChange}
onSlidingStart={onSlidingStart}
onSlidingComplete={onSlidingComplete}
/>
<Text style={styles.durationText}>
{formatTime(duration)}
</Text>
</View>
</View> </View>
{/* Bottom Buttons Row */} {/* Bottom Buttons Row */}
@ -943,25 +1065,40 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
position: 'relative', position: 'relative',
}, },
sliderRow: {
flexDirection: 'row',
alignItems: 'center',
width: '100%',
},
bufferIndicator: { bufferIndicator: {
position: 'absolute', position: 'absolute',
height: 3, height: 4,
backgroundColor: 'rgba(255, 255, 255, 0.3)', backgroundColor: 'rgba(255, 255, 255, 0.25)',
top: 14, // Position to align with the slider track top: 14, // Position to align with the slider track
left: 0, left: 24, // Account for the currentTime text
right: 24, // Account for the duration text
zIndex: 1, zIndex: 1,
borderRadius: 1.5, borderRadius: 2,
}, },
slider: { slider: {
width: '100%', flex: 1,
height: 30, height: 40,
marginHorizontal: 8,
}, },
duration: { currentTime: {
color: 'white', color: 'white',
fontSize: 12, fontSize: 12,
marginTop: 4,
opacity: 0.9,
fontWeight: '500', fontWeight: '500',
opacity: 0.9,
width: 40, // Fix width to prevent layout shifts
textAlign: 'right',
},
durationText: {
color: 'white',
fontSize: 12,
fontWeight: '500',
opacity: 0.9,
width: 40, // Fix width to prevent layout shifts
}, },
bottomButtons: { bottomButtons: {
flexDirection: 'row', flexDirection: 'row',
@ -1143,37 +1280,6 @@ const styles = StyleSheet.create({
selectedOptionText: { selectedOptionText: {
fontWeight: 'bold', fontWeight: 'bold',
}, },
sliderBubble: {
backgroundColor: '#E50914',
borderRadius: 4,
padding: 4,
position: 'absolute',
bottom: 25,
},
sliderBubbleText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
customThumb: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: 'rgba(229, 9, 20, 0.9)',
justifyContent: 'center',
alignItems: 'center',
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 2,
},
thumbInner: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: 'white',
},
}); });
export default VideoPlayer; export default VideoPlayer;

View file

@ -42,6 +42,7 @@ export interface StreamingContent {
inLibrary?: boolean; inLibrary?: boolean;
directors?: string[]; directors?: string[];
creators?: string[]; creators?: string[];
certification?: string;
} }
export interface CatalogContent { export interface CatalogContent {
@ -299,7 +300,8 @@ class CatalogService {
genres: meta.genres, genres: meta.genres,
description: meta.description, description: meta.description,
runtime: meta.runtime, runtime: meta.runtime,
inLibrary: this.library[`${meta.type}:${meta.id}`] !== undefined inLibrary: this.library[`${meta.type}:${meta.id}`] !== undefined,
certification: meta.certification
}; };
} }
@ -407,11 +409,11 @@ class CatalogService {
} }
async searchContentCinemeta(query: string): Promise<StreamingContent[]> { async searchContentCinemeta(query: string): Promise<StreamingContent[]> {
if (!query || query.trim().length < 2) { if (!query) {
return []; return [];
} }
const trimmedQuery = query.trim(); const trimmedQuery = query.trim().toLowerCase();
console.log('Searching Cinemeta for:', trimmedQuery); console.log('Searching Cinemeta for:', trimmedQuery);
const addons = await this.getAllAddons(); const addons = await this.getAllAddons();
@ -421,26 +423,20 @@ class CatalogService {
const cinemeta = addons.find(addon => addon.id === 'com.linvo.cinemeta'); const cinemeta = addons.find(addon => addon.id === 'com.linvo.cinemeta');
if (!cinemeta || !cinemeta.catalogs) { if (!cinemeta || !cinemeta.catalogs) {
console.error('Cinemeta addon not found. Available addons:', addons.map(a => ({ id: a.id, url: a.transportUrl }))); console.error('Cinemeta addon not found');
return []; return [];
} }
console.log('Found Cinemeta addon:', cinemeta.id);
// Search in both movie and series catalogs simultaneously // Search in both movie and series catalogs simultaneously
const searchPromises = ['movie', 'series'].map(async (type) => { const searchPromises = ['movie', 'series'].map(async (type) => {
try { try {
console.log(`Searching ${type} catalog with query:`, trimmedQuery);
// Direct API call to Cinemeta // Direct API call to Cinemeta
const url = `https://v3-cinemeta.strem.io/catalog/${type}/top/search=${encodeURIComponent(trimmedQuery)}.json`; const url = `https://v3-cinemeta.strem.io/catalog/${type}/top/search=${encodeURIComponent(trimmedQuery)}.json`;
console.log('Request URL:', url); console.log('Request URL:', url);
const response = await axios.get<{ metas: Meta[] }>(url); const response = await axios.get<{ metas: any[] }>(url);
const metas = response.data.metas || []; const metas = response.data.metas || [];
console.log(`Found ${metas.length} results for ${type}`);
if (metas && metas.length > 0) { if (metas && metas.length > 0) {
const items = metas.map(meta => this.convertMetaToStreamingContent(meta)); const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
results.push(...items); results.push(...items);
@ -452,15 +448,14 @@ class CatalogService {
await Promise.all(searchPromises); await Promise.all(searchPromises);
console.log('Total results found:', results.length); // Remove duplicates while preserving order
const seen = new Set();
// Sort results by name and ensure uniqueness return results.filter(item => {
const uniqueResults = Array.from( const key = `${item.type}:${item.id}`;
new Map(results.map(item => [`${item.type}:${item.id}`, item])).values() if (seen.has(key)) return false;
); seen.add(key);
uniqueResults.sort((a, b) => a.name.localeCompare(b.name)); return true;
});
return uniqueResults;
} }
async getStremioId(type: string, tmdbId: string): Promise<string | null> { async getStremioId(type: string, tmdbId: string): Promise<string | null> {

View file

@ -0,0 +1,86 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
interface WatchProgress {
currentTime: number;
duration: number;
lastUpdated: number;
}
class StorageService {
private static instance: StorageService;
private readonly WATCH_PROGRESS_KEY = '@watch_progress:';
private constructor() {}
public static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
private getWatchProgressKey(id: string, type: string, episodeId?: string): string {
return this.WATCH_PROGRESS_KEY + `${type}:${id}${episodeId ? `:${episodeId}` : ''}`;
}
public async setWatchProgress(
id: string,
type: string,
progress: WatchProgress,
episodeId?: string
): Promise<void> {
try {
const key = this.getWatchProgressKey(id, type, episodeId);
await AsyncStorage.setItem(key, JSON.stringify(progress));
} catch (error) {
console.error('Error saving watch progress:', error);
}
}
public async getWatchProgress(
id: string,
type: string,
episodeId?: string
): Promise<WatchProgress | null> {
try {
const key = this.getWatchProgressKey(id, type, episodeId);
const data = await AsyncStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Error getting watch progress:', error);
return null;
}
}
public async removeWatchProgress(
id: string,
type: string,
episodeId?: string
): Promise<void> {
try {
const key = this.getWatchProgressKey(id, type, episodeId);
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Error removing watch progress:', error);
}
}
public async getAllWatchProgress(): Promise<Record<string, WatchProgress>> {
try {
const keys = await AsyncStorage.getAllKeys();
const watchProgressKeys = keys.filter(key => key.startsWith(this.WATCH_PROGRESS_KEY));
const pairs = await AsyncStorage.multiGet(watchProgressKeys);
return pairs.reduce((acc, [key, value]) => {
if (value) {
acc[key.replace(this.WATCH_PROGRESS_KEY, '')] = JSON.parse(value);
}
return acc;
}, {} as Record<string, WatchProgress>);
} catch (error) {
console.error('Error getting all watch progress:', error);
return {};
}
}
}
export const storageService = StorageService.getInstance();

View file

@ -18,6 +18,7 @@ export interface Meta {
cast?: string[]; cast?: string[];
director?: string; director?: string;
writer?: string; writer?: string;
certification?: string;
} }
export interface Stream { export interface Stream {

View file

@ -480,6 +480,37 @@ export class TMDBService {
return null; return null;
} }
} }
async getCertification(type: string, id: number): Promise<string | null> {
try {
// Different endpoints for movies and TV shows
const endpoint = type === 'movie' ? 'movie' : 'tv';
const response = await axios.get(`${BASE_URL}/${endpoint}/${id}/release_dates`, {
headers: this.getHeaders()
});
if (response.data && response.data.results) {
// Try to find US certification first
const usRelease = response.data.results.find((r: any) => r.iso_3166_1 === 'US');
if (usRelease && usRelease.release_dates && usRelease.release_dates.length > 0) {
const certification = usRelease.release_dates.find((rd: any) => rd.certification)?.certification;
if (certification) return certification;
}
// Fallback to any certification if US is not available
for (const country of response.data.results) {
if (country.release_dates && country.release_dates.length > 0) {
const certification = country.release_dates.find((rd: any) => rd.certification)?.certification;
if (certification) return certification;
}
}
}
return null;
} catch (error) {
console.error('Error fetching certification:', error);
return null;
}
}
} }
export const tmdbService = TMDBService.getInstance(); export const tmdbService = TMDBService.getInstance();

View file

@ -1,10 +1,11 @@
import { TMDBEpisode } from '../services/tmdbService'; import { TMDBEpisode } from '../services/tmdbService';
// Types for route params // Types for route params
export interface RouteParams { export type RouteParams = {
id: string; id: string;
type: string; type: string;
} episodeId?: string;
};
// Stream related types // Stream related types
export interface Stream { export interface Stream {
@ -92,6 +93,7 @@ export interface StreamingContent {
releaseInfo?: string; releaseInfo?: string;
directors?: string[]; directors?: string[];
creators?: string[]; creators?: string[];
certification?: string;
} }
// Navigation types // Navigation types

5
src/types/svg.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module '*.svg' {
import { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}