23
App.tsx
|
|
@ -21,6 +21,7 @@ import AppNavigator, {
|
|||
import 'react-native-reanimated';
|
||||
import { CatalogProvider } from './src/contexts/CatalogContext';
|
||||
import { GenreProvider } from './src/contexts/GenreContext';
|
||||
import { TraktProvider } from './src/contexts/TraktContext';
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
// Always use dark mode
|
||||
|
|
@ -30,16 +31,18 @@ function App(): React.JSX.Element {
|
|||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GenreProvider>
|
||||
<CatalogProvider>
|
||||
<PaperProvider theme={CustomDarkTheme}>
|
||||
<NavigationContainer theme={CustomNavigationDarkTheme}>
|
||||
<View style={[styles.container, { backgroundColor: '#000000' }]}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
/>
|
||||
<AppNavigator />
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
<TraktProvider>
|
||||
<PaperProvider theme={CustomDarkTheme}>
|
||||
<NavigationContainer theme={CustomNavigationDarkTheme}>
|
||||
<View style={[styles.container, { backgroundColor: '#000000' }]}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
/>
|
||||
<AppNavigator />
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
</TraktProvider>
|
||||
</CatalogProvider>
|
||||
</GenreProvider>
|
||||
</GestureHandlerRootView>
|
||||
|
|
|
|||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) [Year] [Copyright Holder Name]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
158
README.md
|
|
@ -1,91 +1,119 @@
|
|||
# 🎬 NUVIO Streaming App 🎬
|
||||
# Nuvio - Streaming App
|
||||
|
||||
A modern, beautiful streaming application built with React Native and Expo.
|
||||
Nuvio is an Open-Source cross-platform streaming application built with React Native and Expo, allowing users to browse, discover, and watch video content.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🚀 Fast, responsive UI with smooth animations
|
||||
- 🎥 Stream movies and TV shows with ease
|
||||
- 🔍 Powerful search functionality
|
||||
- 📚 Organize your media in a personal library
|
||||
- 📱 Cross-platform (iOS and Android)
|
||||
- 🌙 Beautiful dark mode interface
|
||||
- 🧩 Add-on system for extensibility
|
||||
- 📅 Calendar for upcoming releases
|
||||
- 📺 Video player with quality selection
|
||||
* **Home Screen:** Customizable dashboard featuring highlighted content, continue watching section, and access to various content catalogs.
|
||||
* **Content Discovery:** Explore trending, popular, or categorized movies and TV shows.
|
||||
* **Detailed Metadata:** Access comprehensive information for content, including descriptions, cast, crew, and ratings.
|
||||
* **Catalog Browsing:** Navigate through specific genres, curated lists, or addon-provided catalogs.
|
||||
* **Video Playback:** Integrated video player for watching content.
|
||||
* **Stream Selection:** Choose from available video streams provided by configured sources/addons.
|
||||
* **Search Functionality:** Search for specific movies, TV shows, or other content.
|
||||
* **Personal Library:** Manage a collection of favorite movies and shows.
|
||||
* **Trakt.tv Integration:** Sync watch history, collection, and watch progress with your Trakt account.
|
||||
* **Addon Management:** Install, manage, and reorder addons compatible with the Stremio addon protocol to source content streams and catalogs.
|
||||
* **Release Calendar:** View upcoming movie releases or TV show episode air dates.
|
||||
* **Extensive Settings:**
|
||||
* Player customization (e.g., subtitle preferences).
|
||||
* Content source configuration (TMDB API keys, MDBList URLs).
|
||||
* Catalog management and visibility.
|
||||
* Trakt account connection.
|
||||
* Notification preferences.
|
||||
* Home screen layout adjustments.
|
||||
* **Optimized & Interactive UI:** Smooth browsing with skeleton loaders, pull-to-refresh, performant lists, haptic feedback, and action menus.
|
||||
* **Cross-Platform:** Runs on iOS and Android (highly optimized for iOS; Android performance is generally good).
|
||||
|
||||
## 📱 Screenshots
|
||||
## 📸 Screenshots
|
||||
|
||||
*Coming soon!*
|
||||
| Home | Discover | Search |
|
||||
| :----------------------------------------- | :----------------------------------------- | :--------------------------------------- |
|
||||
|  |  |  |
|
||||
| **Metadata** | **Seasons & Episodes** | **Rating** |
|
||||
|  | |  |
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
## 🚀 Tech Stack
|
||||
|
||||
- React Native
|
||||
- Expo
|
||||
- TypeScript
|
||||
- React Navigation
|
||||
- React Native Paper
|
||||
- Expo Linear Gradient
|
||||
- Expo Vector Icons
|
||||
* **Framework:** React Native (v0.76.9) with Expo (SDK 52)
|
||||
* **Language:** TypeScript
|
||||
* **Navigation:** React Navigation (v7)
|
||||
* **Video Playback:** `react-native-video`
|
||||
* **UI Components:** `react-native-paper`, `@gorhom/bottom-sheet`, `@shopify/flash-list`
|
||||
* **State Management/Async:** Context API, `axios`
|
||||
* **Animations & Gestures:** `react-native-reanimated`, `react-native-gesture-handler`
|
||||
* **Data Sources (Inferred):** TMDB (The Movie Database), potentially Stremio-related services
|
||||
|
||||
## 📋 Requirements
|
||||
## 🛠️ Setup & Running
|
||||
|
||||
- Node.js 14+
|
||||
- Expo CLI
|
||||
- Android Studio (for Android development)
|
||||
- Xcode (for iOS development, macOS only)
|
||||
1. **Prerequisites:**
|
||||
* Node.js (LTS recommended)
|
||||
* npm or yarn
|
||||
* Expo Go app on your device/simulator (for development) or setup for native builds (Android Studio/Xcode).
|
||||
|
||||
## 🚀 Getting Started
|
||||
2. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/nayifleo1/NuvioExpo.git
|
||||
cd nuvio
|
||||
```
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/nayifleo1/NuvioExpo.git
|
||||
cd NuvioExpo
|
||||
```
|
||||
3. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
4. **Run the application:**
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
* **For Expo Go (Development):**
|
||||
```bash
|
||||
npm start
|
||||
# or
|
||||
yarn start
|
||||
```
|
||||
Scan the QR code with the Expo Go app on your iOS or Android device.
|
||||
|
||||
4. Run on your preferred platform:
|
||||
- Press `a` for Android
|
||||
- Press `i` for iOS (requires macOS)
|
||||
- Scan the QR code with Expo Go on your device
|
||||
* **For Native Android Build/Emulator:**
|
||||
```bash
|
||||
npm run android
|
||||
# or
|
||||
yarn android
|
||||
```
|
||||
|
||||
## 🌟 Key Components
|
||||
|
||||
- **Home Screen**: Discover trending and recommended content
|
||||
- **Discover Screen**: Browse through categories and genres
|
||||
- **Library Screen**: Access your saved and watched content
|
||||
- **Add-ons Screen**: Manage streaming sources
|
||||
- **Settings Screen**: Customize your experience
|
||||
|
||||
## 🔮 Future Plans
|
||||
|
||||
- ⚡ Performance optimizations
|
||||
- 🔐 User authentication
|
||||
- 💾 Offline viewing
|
||||
- 📢 Push notifications for new content
|
||||
- 🌐 Multi-language support
|
||||
* **For Native iOS Build/Simulator:**
|
||||
```bash
|
||||
npm run ios
|
||||
# or
|
||||
yarn ios
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions, issues, and feature requests are welcome!
|
||||
Contributions are welcome! If you'd like to contribute, please follow these general steps:
|
||||
|
||||
## 📝 License
|
||||
1. Fork the repository.
|
||||
2. Create a new branch for your feature or bug fix (`git checkout -b feature/your-feature-name` or `bugfix/issue-number`).
|
||||
3. Make your changes and commit them with descriptive messages.
|
||||
4. Push your branch to your fork (`git push origin feature/your-feature-name`).
|
||||
5. Open a Pull Request to the main repository's `main` or `develop` branch (please check which branch is used for development).
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
Please ensure your code follows the project's coding style and includes tests where applicable.
|
||||
|
||||
## 🐛 Reporting Issues
|
||||
|
||||
If you encounter any bugs or have suggestions, please open an issue on the GitHub repository. Provide as much detail as possible, including:
|
||||
|
||||
* Steps to reproduce the issue.
|
||||
* Expected behavior.
|
||||
* Actual behavior.
|
||||
* Screenshots or logs, if helpful.
|
||||
* Your environment (OS, device, app version).
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
Special thanks to all the open-source libraries and tools that made this project possible.
|
||||
Huge thanks to the Stremio team for their pioneering work in the streaming space and for creating their addon protocol/system. As an indie developer, their approach has been a major source of inspiration. This project utilizes compatibility with the Stremio addon ecosystem to source content.
|
||||
|
||||
---
|
||||
## 📄 License
|
||||
|
||||
Built with ❤️ by the NUVIO team
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
16
android/.gitignore
vendored
|
|
@ -1,16 +0,0 @@
|
|||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'com.nuvio.app'
|
||||
defaultConfig {
|
||||
applicationId 'com.nuvio.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
14
android/app/proguard-rules.pro
vendored
|
|
@ -1,14 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# react-native-reanimated
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="com.nuvio.app"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.nuvio.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package com.nuvio.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
)
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1 +0,0 @@
|
|||
<resources/>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="iconBackground">#ffffff</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
</resources>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">Nuvio</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||
</resources>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
<item name="android:editTextStyle">@style/ResetEditText</item>
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">#ffffff</item>
|
||||
</style>
|
||||
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Enable AAPT2 PNG crunching
|
||||
android.enablePngCrunchInReleaseBuilds=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=true
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
|
||||
# Enable network inspector
|
||||
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
|
|
@ -1,7 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
252
android/gradlew
vendored
|
|
@ -1,252 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
android/gradlew.bat
vendored
|
|
@ -1,94 +0,0 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
pluginManagement {
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString())
|
||||
}
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
def command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'android'
|
||||
].toList()
|
||||
ex.autolinkLibrariesFromCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'Nuvio'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
reactAndroidLibs {
|
||||
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
|
||||
useExpoModules()
|
||||
|
||||
include ':app'
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())
|
||||
2
assets/trakt-logo.png
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// This is a placeholder for a binary PNG file
|
||||
// Replace this file with an actual Trakt logo image
|
||||
30
ios/.gitignore
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
.xcode.env.local
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
|
||||
# CocoaPods
|
||||
/Pods/
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
||||
|
|
@ -1,471 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
96905EF65AED1B983A6B3ABC /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Nuvio.a */; };
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
53F59A3E794F4B87B221D4C3 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1EB3A22DCB47A0A12A0CAF /* noop-file.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Nuvio/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Nuvio/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Nuvio/main.m; sourceTree = "<group>"; };
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6C2E3173556A471DD304B334 /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
DC1EB3A22DCB47A0A12A0CAF /* noop-file.swift */ = {isa = PBXFileReference; name = "noop-file.swift"; path = "Nuvio/noop-file.swift"; sourceTree = "<group>"; fileEncoding = 4; lastKnownFileType = sourcecode.swift; explicitFileType = undefined; includeInIndex = 0; };
|
||||
DD9A8A1855A34F10923D3C37 /* Nuvio-Bridging-Header.h */ = {isa = PBXFileReference; name = "Nuvio-Bridging-Header.h"; path = "Nuvio/Nuvio-Bridging-Header.h"; sourceTree = "<group>"; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
96905EF65AED1B983A6B3ABC /* libPods-Nuvio.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* Nuvio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
DC1EB3A22DCB47A0A12A0CAF /* noop-file.swift */,
|
||||
DD9A8A1855A34F10923D3C37 /* Nuvio-Bridging-Header.h */,
|
||||
);
|
||||
name = Nuvio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Nuvio.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* Nuvio */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */,
|
||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* Nuvio.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
92DBD88DE9BF7D494EA9DA96 /* Nuvio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = Nuvio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */,
|
||||
);
|
||||
name = Supporting;
|
||||
path = Nuvio/Supporting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6C2E3173556A471DD304B334 /* Pods-Nuvio.debug.xcconfig */,
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-Nuvio.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
92DBD88DE9BF7D494EA9DA96 /* Nuvio */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* Nuvio */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Nuvio;
|
||||
productName = Nuvio;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* Nuvio.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1130;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1250;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nuvio" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* Nuvio */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
8A293A5AD8A6486B86EDC6E7 /* Nuvio-Bridging-Header.h in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||
53F59A3E794F4B87B221D4C3 /* noop-file.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Nuvio.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Nuvio/Nuvio-Bridging-Header.h;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Nuvio.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Nuvio/Nuvio-Bridging-Header.h;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nuvio" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nuvio.app"
|
||||
BlueprintName = "Nuvio"
|
||||
ReferencedContainer = "container:Nuvio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "NuvioTests.xctest"
|
||||
BlueprintName = "NuvioTests"
|
||||
ReferencedContainer = "container:Nuvio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nuvio.app"
|
||||
BlueprintName = "Nuvio"
|
||||
ReferencedContainer = "container:Nuvio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nuvio.app"
|
||||
BlueprintName = "Nuvio"
|
||||
ReferencedContainer = "container:Nuvio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#import <RCTAppDelegate.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Expo/Expo.h>
|
||||
|
||||
@interface AppDelegate : EXAppDelegateWrapper
|
||||
|
||||
@end
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#import "AppDelegate.h"
|
||||
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTLinkingManager.h>
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
self.moduleName = @"main";
|
||||
|
||||
// You can add your custom initial props in the dictionary below.
|
||||
// They will be passed down to the ViewController used by React Native.
|
||||
self.initialProps = @{};
|
||||
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
return [self bundleURL];
|
||||
}
|
||||
|
||||
- (NSURL *)bundleURL
|
||||
{
|
||||
#if DEBUG
|
||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
|
||||
#else
|
||||
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Linking API
|
||||
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
||||
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
|
||||
}
|
||||
|
||||
// Universal Links
|
||||
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
|
||||
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
|
||||
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
|
||||
}
|
||||
|
||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
||||
{
|
||||
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
||||
}
|
||||
|
||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
||||
{
|
||||
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
|
||||
}
|
||||
|
||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||
{
|
||||
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
|
||||
}
|
||||
|
||||
@end
|
||||
|
Before Width: | Height: | Size: 907 KiB |
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "App-Icon-1024x1024@1x.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "expo"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "1.00000000000000",
|
||||
"green": "1.00000000000000",
|
||||
"red": "1.00000000000000"
|
||||
},
|
||||
"color-space": "srgb"
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "image@3x.png",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
|
@ -1,74 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<scene sceneID="EXPO-SCENE-1">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
|
||||
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
||||
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
||||
<constraints>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
|
||||
</constraints>
|
||||
<color key="backgroundColor" name="SplashScreenBackground"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="SplashScreenLogo" width="414" height="736"/>
|
||||
<namedColor name="SplashScreenBackground">
|
||||
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<false/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
//
|
||||
// @generated
|
||||
// A blank Swift file must be created for native modules with Swift files to work correctly.
|
||||
//
|
||||
66
ios/Podfile
|
|
@ -1,66 +0,0 @@
|
|||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||
|
||||
require 'json'
|
||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
|
||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||
install! 'cocoapods',
|
||||
:deterministic_uuids => false
|
||||
|
||||
prepare_react_native_project!
|
||||
|
||||
target 'Nuvio' do
|
||||
use_expo_modules!
|
||||
|
||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||
else
|
||||
config_command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'ios'
|
||||
]
|
||||
end
|
||||
|
||||
config = use_native_modules!(config_command)
|
||||
|
||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||
)
|
||||
|
||||
# This is necessary for Xcode 14, because it signs resource bundles by default
|
||||
# when building for devices.
|
||||
installer.target_installation_results.pod_target_installation_results
|
||||
.each do |pod_name, target_installation_result|
|
||||
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
|
||||
resource_bundle_target.build_configurations.each do |config|
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"expo.jsEngine": "hermes",
|
||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||
"newArchEnabled": "true"
|
||||
}
|
||||
142
package-lock.json
generated
|
|
@ -14,6 +14,8 @@
|
|||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/slider": "^4.5.6",
|
||||
"@react-native-masked-view/masked-view": "github:react-native-masked-view/masked-view",
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@react-navigation/native-stack": "^7.3.10",
|
||||
|
|
@ -23,7 +25,9 @@
|
|||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.43",
|
||||
"expo-auth-session": "^6.0.3",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-file-system": "^18.0.12",
|
||||
"expo-haptics": "~14.0.1",
|
||||
|
|
@ -31,13 +35,16 @@
|
|||
"expo-intent-launcher": "~12.0.2",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-notifications": "~0.29.14",
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-screen-orientation": "~8.0.4",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "^4.0.9",
|
||||
"expo-web-browser": "^14.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-awesome-slider": "^2.9.0",
|
||||
"react-native-draggable-flatlist": "^4.0.2",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
|
|
@ -47,6 +54,7 @@
|
|||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-tab-view": "^4.0.10",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"subsrt": "^1.1.1"
|
||||
|
|
@ -3332,6 +3340,28 @@
|
|||
"integrity": "sha512-UhLPFeqx0YfPLrEz8ffT3uqAyXWu6iqFjohNsbp4cOU7hnJwg2RXtDnYHoHMr7MOkZDVdlLMdrSrAuzY6KGqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-native-masked-view/masked-view": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "git+ssh://git@github.com/react-native-masked-view/masked-view.git#14df52650be2441fbf6f2a0308cc54a62e68820c",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-native": ">=0.57"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz",
|
||||
"integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
|
||||
|
|
@ -6457,6 +6487,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exec-async": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
||||
|
|
@ -6629,6 +6665,24 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-auth-session": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-6.0.3.tgz",
|
||||
"integrity": "sha512-s7LmmMPiiY1NXrlcXkc4+09Hlfw9X1CpaQOCDkwfQEodG1uCYGQi/WImTnDzw5YDkWI79uC8F1mB8EIerilkDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-application": "~6.0.2",
|
||||
"expo-constants": "~17.0.5",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-web-browser": "~14.0.2",
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-blur": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
||||
|
|
@ -6654,6 +6708,18 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.0.2.tgz",
|
||||
"integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-file-system": {
|
||||
"version": "18.0.12",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz",
|
||||
|
|
@ -6736,6 +6802,20 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-linking": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.0.5.tgz",
|
||||
"integrity": "sha512-3KptlJtcYDPWohk0MfJU75MJFh2ybavbtcSd84zEPfw9s1q3hjimw3sXnH03ZxP54kiEWldvKmmnGcVffBDB1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-constants": "~17.0.5",
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-modules-autolinking": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz",
|
||||
|
|
@ -6820,6 +6900,19 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-random": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expo-random/-/expo-random-14.0.1.tgz",
|
||||
"integrity": "sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==",
|
||||
"deprecated": "This package is now deprecated in favor of expo-crypto, which provides the same functionality. To migrate, replace all imports from expo-random with imports from expo-crypto.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-screen-orientation": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-8.0.4.tgz",
|
||||
|
|
@ -6866,6 +6959,16 @@
|
|||
"integrity": "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo-web-browser": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.0.2.tgz",
|
||||
"integrity": "sha512-Hncv2yojhTpHbP6SGWARBFdl7P6wBHc1O8IKaNsH0a/IEakq887o1eRhLxZ5IwztPQyRDhpqHdgJ+BjWolOnwA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/exponential-backoff": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
|
||||
|
|
@ -10617,6 +10720,20 @@
|
|||
"react-native-reanimated": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-draggable-flatlist": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.2.tgz",
|
||||
"integrity": "sha512-6WN/o3WIRIJdcrQtwGju5vtXjfK8KTFtXds10QIT00MP9SxEFt5VoX7QW+JC22Rpk651cNScOVm+WKs7vDV0iw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.17.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.64.0",
|
||||
"react-native-gesture-handler": ">=2.0.0",
|
||||
"react-native-reanimated": ">=2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-gesture-handler": {
|
||||
"version": "2.20.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||
|
|
@ -10671,6 +10788,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-pager-view": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.7.0.tgz",
|
||||
"integrity": "sha512-sutxKiMqBuQrEyt4mLaLNzy8taIC7IuYpxfcwQBXfSYBSSpAa0qE9G1FXlP/iXqTSlFgBXyK7BESsl9umOjECQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "5.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.1.tgz",
|
||||
|
|
@ -10802,6 +10930,20 @@
|
|||
"react-native-svg": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-tab-view": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.0.10.tgz",
|
||||
"integrity": "sha512-KU1ovavUURfKffqNn7F2jwgQ0tUSa2WosnHSztVYArCr22HP2nR7xHrd8DddFL4uenaT9KGXlNgx1IUPGUdZSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-latest-callback": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-pager-view": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/slider": "^4.5.6",
|
||||
"@react-native-masked-view/masked-view": "github:react-native-masked-view/masked-view",
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@react-navigation/native-stack": "^7.3.10",
|
||||
|
|
@ -24,7 +26,9 @@
|
|||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.43",
|
||||
"expo-auth-session": "^6.0.3",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-file-system": "^18.0.12",
|
||||
"expo-haptics": "~14.0.1",
|
||||
|
|
@ -32,13 +36,16 @@
|
|||
"expo-intent-launcher": "~12.0.2",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-notifications": "~0.29.14",
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-screen-orientation": "~8.0.4",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "^4.0.9",
|
||||
"expo-web-browser": "^14.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-awesome-slider": "^2.9.0",
|
||||
"react-native-draggable-flatlist": "^4.0.2",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
|
|
@ -48,6 +55,7 @@
|
|||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-tab-view": "^4.0.10",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"subsrt": "^1.1.1"
|
||||
|
|
|
|||
BIN
src/assets/discover.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
src/assets/home.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
src/assets/metadascreen.jpg
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
src/assets/ratingscreen.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
src/assets/search.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
src/assets/seasonandepisode.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
|
@ -37,6 +37,9 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
const isTablet = width > 768;
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const [episodeProgress, setEpisodeProgress] = useState<{ [key: string]: { currentTime: number; duration: number } }>({});
|
||||
|
||||
// Add ref for the season selector ScrollView
|
||||
const seasonScrollViewRef = useRef<ScrollView | null>(null);
|
||||
|
||||
const loadEpisodesProgress = async () => {
|
||||
if (!metadata?.id) return;
|
||||
|
|
@ -70,6 +73,25 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
}, [episodes, metadata?.id])
|
||||
);
|
||||
|
||||
// Add effect to scroll to selected season
|
||||
useEffect(() => {
|
||||
if (selectedSeason && seasonScrollViewRef.current && Object.keys(groupedEpisodes).length > 0) {
|
||||
// Find the index of the selected season
|
||||
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b);
|
||||
const selectedIndex = seasons.findIndex(season => season === selectedSeason);
|
||||
|
||||
if (selectedIndex !== -1) {
|
||||
// Wait a small amount of time for layout to be ready
|
||||
setTimeout(() => {
|
||||
seasonScrollViewRef.current?.scrollTo({
|
||||
x: selectedIndex * 116, // 100px width + 16px margin
|
||||
animated: true
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}, [selectedSeason, groupedEpisodes]);
|
||||
|
||||
if (loadingSeasons) {
|
||||
return (
|
||||
<View style={styles.centeredContainer}>
|
||||
|
|
@ -99,6 +121,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
<View style={styles.seasonSelectorWrapper}>
|
||||
<Text style={styles.seasonSelectorTitle}>Seasons</Text>
|
||||
<ScrollView
|
||||
ref={seasonScrollViewRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.seasonSelectorContainer}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { StreamingContent } from '../services/catalogService';
|
||||
import { addonEmitter, ADDON_EVENTS } from '../services/stremioService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface CatalogContextType {
|
||||
lastUpdate: number;
|
||||
|
|
@ -25,8 +27,29 @@ export const CatalogProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
|||
|
||||
const refreshCatalogs = useCallback(() => {
|
||||
setLastUpdate(Date.now());
|
||||
logger.info('Refreshing catalogs, timestamp:', Date.now());
|
||||
}, []);
|
||||
|
||||
// Listen for addon changes to update catalog data
|
||||
useEffect(() => {
|
||||
const handleAddonChange = () => {
|
||||
logger.info('Addon changed, triggering catalog refresh');
|
||||
refreshCatalogs();
|
||||
};
|
||||
|
||||
// Subscribe to all addon events to refresh catalogs
|
||||
addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
|
||||
return () => {
|
||||
// Clean up event listeners
|
||||
addonEmitter.off(ADDON_EVENTS.ORDER_CHANGED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
};
|
||||
}, [refreshCatalogs]);
|
||||
|
||||
const addToLibrary = useCallback((content: StreamingContent) => {
|
||||
setLibraryItems(prev => [...prev, content]);
|
||||
}, []);
|
||||
|
|
|
|||
37
src/contexts/TraktContext.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { useTraktIntegration } from '../hooks/useTraktIntegration';
|
||||
import { TraktUser, TraktWatchedItem } from '../services/traktService';
|
||||
|
||||
interface TraktContextProps {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
userProfile: TraktUser | null;
|
||||
watchedMovies: TraktWatchedItem[];
|
||||
watchedShows: TraktWatchedItem[];
|
||||
checkAuthStatus: () => Promise<void>;
|
||||
loadWatchedItems: () => Promise<void>;
|
||||
isMovieWatched: (imdbId: string) => Promise<boolean>;
|
||||
isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise<boolean>;
|
||||
markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise<boolean>;
|
||||
markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const TraktContext = createContext<TraktContextProps | undefined>(undefined);
|
||||
|
||||
export function TraktProvider({ children }: { children: ReactNode }) {
|
||||
const traktIntegration = useTraktIntegration();
|
||||
|
||||
return (
|
||||
<TraktContext.Provider value={traktIntegration}>
|
||||
{children}
|
||||
</TraktContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTraktContext() {
|
||||
const context = useContext(TraktContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTraktContext must be used within a TraktProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
57
src/hooks/useCustomCatalogNames.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||
|
||||
interface CustomNamesCache {
|
||||
names: { [key: string]: string };
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
// Simple in-memory cache to avoid repeated AsyncStorage reads within the same session
|
||||
let cache: CustomNamesCache | null = null;
|
||||
|
||||
export function useCustomCatalogNames() {
|
||||
const [customNames, setCustomNames] = useState<{ [key: string]: string } | null>(cache?.names || null);
|
||||
const [isLoading, setIsLoading] = useState(!cache); // Only loading if cache is empty
|
||||
|
||||
const loadCustomNames = useCallback(async () => {
|
||||
// Check if cache is recent enough (e.g., within last 5 minutes) - adjust as needed
|
||||
const now = Date.now();
|
||||
if (cache && (now - cache.lastUpdate < 5 * 60 * 1000)) {
|
||||
if (!customNames) setCustomNames(cache.names); // Ensure state is updated if cache existed
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
|
||||
const loadedNames = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
|
||||
setCustomNames(loadedNames);
|
||||
// Update cache
|
||||
cache = { names: loadedNames, lastUpdate: now };
|
||||
} catch (error) {
|
||||
logger.error('Failed to load custom catalog names:', error);
|
||||
setCustomNames({}); // Set to empty object on error to avoid breaking lookups
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []); // Removed customNames dependency to prevent re-running loop
|
||||
|
||||
useEffect(() => {
|
||||
loadCustomNames();
|
||||
}, [loadCustomNames]); // Load on mount and if load function changes
|
||||
|
||||
const getCustomName = useCallback((addonId: string, type: string, catalogId: string, originalName: string): string => {
|
||||
if (isLoading || !customNames) {
|
||||
// Return original name while loading or if loading failed
|
||||
return originalName;
|
||||
}
|
||||
const key = `${addonId}:${type}:${catalogId}`;
|
||||
return customNames[key] || originalName;
|
||||
}, [customNames, isLoading]);
|
||||
|
||||
return { getCustomName, isLoadingCustomNames: isLoading, refreshCustomNames: loadCustomNames };
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
|||
import { CatalogContent, catalogService } from '../services/catalogService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { addonEmitter, ADDON_EVENTS } from '../services/stremioService';
|
||||
|
||||
export function useHomeCatalogs() {
|
||||
const [catalogs, setCatalogs] = useState<CatalogContent[]>([]);
|
||||
|
|
@ -72,6 +73,33 @@ export function useHomeCatalogs() {
|
|||
loadCatalogs();
|
||||
}, [loadCatalogs, lastUpdate]);
|
||||
|
||||
// Subscribe to addon events to refresh catalogs when addons change
|
||||
useEffect(() => {
|
||||
// Handler for addon order changes
|
||||
const handleAddonOrderChange = () => {
|
||||
logger.info('Addon order changed, refreshing catalogs');
|
||||
loadCatalogs();
|
||||
};
|
||||
|
||||
// Handler for addon added/removed
|
||||
const handleAddonChange = () => {
|
||||
logger.info('Addon added or removed, refreshing catalogs');
|
||||
loadCatalogs();
|
||||
};
|
||||
|
||||
// Subscribe to addon events
|
||||
addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, handleAddonOrderChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
addonEmitter.off(ADDON_EVENTS.ORDER_CHANGED, handleAddonOrderChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
};
|
||||
}, [loadCatalogs]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { cacheService } from '../services/cacheService';
|
|||
import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { usePersistentSeasons } from './usePersistentSeasons';
|
||||
|
||||
// Constants for timeouts and retries
|
||||
const API_TIMEOUT = 10000; // 10 seconds
|
||||
|
|
@ -86,6 +87,7 @@ interface UseMetadataReturn {
|
|||
recommendations: StreamingContent[];
|
||||
loadingRecommendations: boolean;
|
||||
setMetadata: React.Dispatch<React.SetStateAction<StreamingContent | null>>;
|
||||
imdbId: string | null;
|
||||
}
|
||||
|
||||
export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn => {
|
||||
|
|
@ -110,6 +112,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
const [loadAttempts, setLoadAttempts] = useState(0);
|
||||
const [recommendations, setRecommendations] = useState<StreamingContent[]>([]);
|
||||
const [loadingRecommendations, setLoadingRecommendations] = useState(false);
|
||||
const [imdbId, setImdbId] = useState<string | null>(null);
|
||||
|
||||
// Add hook for persistent seasons
|
||||
const { getSeason, saveSeason } = usePersistentSeasons();
|
||||
|
||||
const processStremioSource = async (type: string, id: string, isEpisode = false) => {
|
||||
const sourceStartTime = Date.now();
|
||||
|
|
@ -316,6 +322,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
if (imdbId) {
|
||||
// Use the imdbId for compatibility with the rest of the app
|
||||
actualId = imdbId;
|
||||
setImdbId(imdbId);
|
||||
// Also store the TMDB ID for later use
|
||||
setTmdbId(parseInt(tmdbId));
|
||||
} else {
|
||||
|
|
@ -394,6 +401,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
if (imdbId) {
|
||||
// Use the imdbId for compatibility with the rest of the app
|
||||
actualId = imdbId;
|
||||
setImdbId(imdbId);
|
||||
// Also store the TMDB ID for later use
|
||||
setTmdbId(parseInt(tmdbId));
|
||||
} else {
|
||||
|
|
@ -471,6 +479,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
catalogService.getContentDetails(type, actualId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
// Store the actual ID used (could be IMDB)
|
||||
if (actualId.startsWith('tt')) {
|
||||
setImdbId(actualId);
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
// Start loading cast immediately in parallel
|
||||
|
|
@ -567,10 +579,17 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
|
||||
setGroupedEpisodes(transformedEpisodes);
|
||||
|
||||
// Get the first available season as fallback
|
||||
const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number));
|
||||
const initialEpisodes = transformedEpisodes[firstSeason] || [];
|
||||
setSelectedSeason(firstSeason);
|
||||
setEpisodes(initialEpisodes);
|
||||
|
||||
// Get saved season from persistence, fallback to first season if not found
|
||||
const persistedSeason = getSeason(id, firstSeason);
|
||||
|
||||
// Set the selected season from persistence
|
||||
setSelectedSeason(persistedSeason);
|
||||
|
||||
// Set episodes for the selected season
|
||||
setEpisodes(transformedEpisodes[persistedSeason] || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load episodes:', error);
|
||||
|
|
@ -950,9 +969,14 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
|
||||
const handleSeasonChange = useCallback((seasonNumber: number) => {
|
||||
if (selectedSeason === seasonNumber) return;
|
||||
|
||||
// Update local state
|
||||
setSelectedSeason(seasonNumber);
|
||||
setEpisodes(groupedEpisodes[seasonNumber] || []);
|
||||
}, [selectedSeason, groupedEpisodes]);
|
||||
|
||||
// Persist the selection
|
||||
saveSeason(id, seasonNumber);
|
||||
}, [selectedSeason, groupedEpisodes, saveSeason, id]);
|
||||
|
||||
const toggleLibrary = useCallback(() => {
|
||||
if (!metadata) return;
|
||||
|
|
@ -1096,5 +1120,6 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
recommendations,
|
||||
loadingRecommendations,
|
||||
setMetadata,
|
||||
imdbId,
|
||||
};
|
||||
};
|
||||
85
src/hooks/usePersistentSeasons.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const SEASONS_STORAGE_KEY = 'selected_seasons';
|
||||
|
||||
interface SeasonsCache {
|
||||
seasons: { [seriesId: string]: number };
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
// Simple in-memory cache to avoid repeated AsyncStorage reads within the same session
|
||||
let cache: SeasonsCache | null = null;
|
||||
|
||||
export function usePersistentSeasons() {
|
||||
const [selectedSeasons, setSelectedSeasons] = useState<{ [seriesId: string]: number } | null>(cache?.seasons || null);
|
||||
const [isLoading, setIsLoading] = useState(!cache); // Only loading if cache is empty
|
||||
|
||||
const loadSelectedSeasons = useCallback(async () => {
|
||||
// Check if cache is recent enough (within last 5 minutes)
|
||||
const now = Date.now();
|
||||
if (cache && (now - cache.lastUpdate < 5 * 60 * 1000)) {
|
||||
if (!selectedSeasons) setSelectedSeasons(cache.seasons); // Ensure state is updated if cache existed
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const savedSeasonsJson = await AsyncStorage.getItem(SEASONS_STORAGE_KEY);
|
||||
const loadedSeasons = savedSeasonsJson ? JSON.parse(savedSeasonsJson) : {};
|
||||
setSelectedSeasons(loadedSeasons);
|
||||
// Update cache
|
||||
cache = { seasons: loadedSeasons, lastUpdate: now };
|
||||
} catch (error) {
|
||||
logger.error('Failed to load persistent seasons:', error);
|
||||
setSelectedSeasons({}); // Set to empty object on error
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSelectedSeasons();
|
||||
}, [loadSelectedSeasons]);
|
||||
|
||||
const saveSeason = useCallback(async (seriesId: string, seasonNumber: number) => {
|
||||
if (!selectedSeasons) return;
|
||||
|
||||
try {
|
||||
const updatedSeasons = {
|
||||
...selectedSeasons,
|
||||
[seriesId]: seasonNumber
|
||||
};
|
||||
|
||||
// Update the cache
|
||||
cache = {
|
||||
seasons: updatedSeasons,
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
|
||||
// Update state
|
||||
setSelectedSeasons(updatedSeasons);
|
||||
|
||||
// Save to AsyncStorage
|
||||
await AsyncStorage.setItem(SEASONS_STORAGE_KEY, JSON.stringify(updatedSeasons));
|
||||
} catch (error) {
|
||||
logger.error('Failed to save selected season:', error);
|
||||
}
|
||||
}, [selectedSeasons]);
|
||||
|
||||
const getSeason = useCallback((seriesId: string, defaultSeason: number = 1): number => {
|
||||
if (isLoading || !selectedSeasons) {
|
||||
return defaultSeason;
|
||||
}
|
||||
return selectedSeasons[seriesId] || defaultSeason;
|
||||
}, [selectedSeasons, isLoading]);
|
||||
|
||||
return {
|
||||
getSeason,
|
||||
saveSeason,
|
||||
isLoadingSeasons: isLoading,
|
||||
refreshSeasons: loadSelectedSeasons
|
||||
};
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ export interface AppSettings {
|
|||
enableBackgroundPlayback: boolean;
|
||||
cacheLimit: number;
|
||||
useExternalPlayer: boolean;
|
||||
preferredPlayer: 'internal' | 'vlc' | 'infuse' | 'outplayer' | 'vidhub' | 'external';
|
||||
showHeroSection: boolean;
|
||||
featuredContentSource: 'tmdb' | 'catalogs';
|
||||
selectedHeroCatalogs: string[]; // Array of catalog IDs to display in hero section
|
||||
|
|
@ -41,6 +42,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
enableBackgroundPlayback: false,
|
||||
cacheLimit: 1024,
|
||||
useExternalPlayer: false,
|
||||
preferredPlayer: 'internal',
|
||||
showHeroSection: true,
|
||||
featuredContentSource: 'tmdb',
|
||||
selectedHeroCatalogs: [], // Empty array means all catalogs are selected
|
||||
|
|
@ -75,14 +77,17 @@ export const useSettings = () => {
|
|||
|
||||
const updateSetting = useCallback(async <K extends keyof AppSettings>(
|
||||
key: K,
|
||||
value: AppSettings[K]
|
||||
value: AppSettings[K],
|
||||
emitEvent: boolean = true
|
||||
) => {
|
||||
const newSettings = { ...settings, [key]: value };
|
||||
try {
|
||||
await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings));
|
||||
setSettings(newSettings);
|
||||
// Notify all subscribers that settings have changed
|
||||
settingsEmitter.emit();
|
||||
// Notify all subscribers that settings have changed (if requested)
|
||||
if (emitEvent) {
|
||||
settingsEmitter.emit();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
}
|
||||
|
|
|
|||
146
src/hooks/useTraktIntegration.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { traktService, TraktUser, TraktWatchedItem } from '../services/traktService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export function useTraktIntegration() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
||||
const [watchedMovies, setWatchedMovies] = useState<TraktWatchedItem[]>([]);
|
||||
const [watchedShows, setWatchedShows] = useState<TraktWatchedItem[]>([]);
|
||||
|
||||
// Check authentication status
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const authenticated = await traktService.isAuthenticated();
|
||||
setIsAuthenticated(authenticated);
|
||||
|
||||
if (authenticated) {
|
||||
const profile = await traktService.getUserProfile();
|
||||
setUserProfile(profile);
|
||||
} else {
|
||||
setUserProfile(null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error checking auth status:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load watched items
|
||||
const loadWatchedItems = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [movies, shows] = await Promise.all([
|
||||
traktService.getWatchedMovies(),
|
||||
traktService.getWatchedShows()
|
||||
]);
|
||||
setWatchedMovies(movies);
|
||||
setWatchedShows(shows);
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error loading watched items:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Check if a movie is watched
|
||||
const isMovieWatched = useCallback(async (imdbId: string): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
return await traktService.isMovieWatched(imdbId);
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error checking if movie is watched:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Check if an episode is watched
|
||||
const isEpisodeWatched = useCallback(async (
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number
|
||||
): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
return await traktService.isEpisodeWatched(imdbId, season, episode);
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error checking if episode is watched:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Mark a movie as watched
|
||||
const markMovieAsWatched = useCallback(async (
|
||||
imdbId: string,
|
||||
watchedAt: Date = new Date()
|
||||
): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const result = await traktService.addToWatchedMovies(imdbId, watchedAt);
|
||||
if (result) {
|
||||
// Refresh watched movies list
|
||||
await loadWatchedItems();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error marking movie as watched:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated, loadWatchedItems]);
|
||||
|
||||
// Mark an episode as watched
|
||||
const markEpisodeAsWatched = useCallback(async (
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
watchedAt: Date = new Date()
|
||||
): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const result = await traktService.addToWatchedEpisodes(imdbId, season, episode, watchedAt);
|
||||
if (result) {
|
||||
// Refresh watched shows list
|
||||
await loadWatchedItems();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error marking episode as watched:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated, loadWatchedItems]);
|
||||
|
||||
// Initialize and check auth status
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
// Load watched items when authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadWatchedItems();
|
||||
}
|
||||
}, [isAuthenticated, loadWatchedItems]);
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
userProfile,
|
||||
watchedMovies,
|
||||
watchedShows,
|
||||
checkAuthStatus,
|
||||
loadWatchedItems,
|
||||
isMovieWatched,
|
||||
isEpisodeWatched,
|
||||
markMovieAsWatched,
|
||||
markEpisodeAsWatched
|
||||
};
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@ import MDBListSettingsScreen from '../screens/MDBListSettingsScreen';
|
|||
import TMDBSettingsScreen from '../screens/TMDBSettingsScreen';
|
||||
import HomeScreenSettings from '../screens/HomeScreenSettings';
|
||||
import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
|
||||
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
||||
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
||||
|
||||
// Stack navigator types
|
||||
export type RootStackParamList = {
|
||||
|
|
@ -85,6 +87,8 @@ export type RootStackParamList = {
|
|||
TMDBSettings: undefined;
|
||||
HomeScreenSettings: undefined;
|
||||
HeroCatalogs: undefined;
|
||||
TraktSettings: undefined;
|
||||
PlayerSettings: undefined;
|
||||
};
|
||||
|
||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
|
@ -651,12 +655,12 @@ const AppNavigator = () => {
|
|||
options={{
|
||||
animation: 'fade',
|
||||
animationDuration: 200,
|
||||
presentation: 'card',
|
||||
...(Platform.OS === 'ios' && { presentation: 'modal' }),
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -698,6 +702,36 @@ const AppNavigator = () => {
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TraktSettings"
|
||||
component={TraktSettingsScreen}
|
||||
options={{
|
||||
animation: 'fade',
|
||||
animationDuration: 200,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PlayerSettings"
|
||||
component={PlayerSettingsScreen}
|
||||
options={{
|
||||
animation: 'fade',
|
||||
animationDuration: 200,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</PaperProvider>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import {
|
|||
Dimensions,
|
||||
ScrollView,
|
||||
useColorScheme,
|
||||
Switch
|
||||
Switch,
|
||||
Linking
|
||||
} from 'react-native';
|
||||
import { stremioService, Manifest } from '../services/stremioService';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -30,10 +31,23 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { logger } from '../utils/logger';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import axios from 'axios';
|
||||
|
||||
// Extend Manifest type to include logo
|
||||
// Extend Manifest type to include logo only (remove disabled status)
|
||||
interface ExtendedManifest extends Manifest {
|
||||
logo?: string;
|
||||
transport?: string;
|
||||
behaviorHints?: {
|
||||
configurable?: boolean;
|
||||
configurationRequired?: boolean;
|
||||
configurationURL?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Interface for Community Addon structure from the JSON URL
|
||||
interface CommunityAddon {
|
||||
transportUrl: string;
|
||||
manifest: ExtendedManifest;
|
||||
}
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
|
@ -49,20 +63,27 @@ const AddonsScreen = () => {
|
|||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [catalogCount, setCatalogCount] = useState(0);
|
||||
const [activeAddons, setActiveAddons] = useState(0);
|
||||
// Add state for reorder mode
|
||||
const [reorderMode, setReorderMode] = useState(false);
|
||||
// Force dark mode
|
||||
const isDarkMode = true;
|
||||
|
||||
// State for community addons
|
||||
const [communityAddons, setCommunityAddons] = useState<CommunityAddon[]>([]);
|
||||
const [communityLoading, setCommunityLoading] = useState(true);
|
||||
const [communityError, setCommunityError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
}, []);
|
||||
|
||||
const loadAddons = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Use the regular method without disabled state
|
||||
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
||||
setAddons(installedAddons);
|
||||
setActiveAddons(installedAddons.length);
|
||||
setAddons(installedAddons as ExtendedManifest[]);
|
||||
|
||||
// Count catalogs
|
||||
let totalCatalogs = 0;
|
||||
|
|
@ -91,28 +112,46 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async () => {
|
||||
if (!addonUrl) {
|
||||
Alert.alert('Error', 'Please enter an addon URL');
|
||||
// Function to load community addons
|
||||
const loadCommunityAddons = async () => {
|
||||
setCommunityLoading(true);
|
||||
setCommunityError(null);
|
||||
try {
|
||||
const response = await axios.get<CommunityAddon[]>('https://stremio-addons.com/catalog.json');
|
||||
// Filter out addons without a manifest or transportUrl (basic validation)
|
||||
const validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
||||
setCommunityAddons(validAddons);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load community addons:', error);
|
||||
setCommunityError('Failed to load community addons. Please try again later.');
|
||||
} finally {
|
||||
setCommunityLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async (url?: string) => {
|
||||
const urlToInstall = url || addonUrl;
|
||||
if (!urlToInstall) {
|
||||
Alert.alert('Error', 'Please enter an addon URL or select a community addon');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setInstalling(true);
|
||||
// First fetch the addon manifest
|
||||
const manifest = await stremioService.getManifest(addonUrl);
|
||||
const manifest = await stremioService.getManifest(urlToInstall);
|
||||
setAddonDetails(manifest);
|
||||
setAddonUrl(urlToInstall);
|
||||
setShowConfirmModal(true);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch addon details:', error);
|
||||
Alert.alert('Error', 'Failed to fetch addon details');
|
||||
Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`);
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmInstallAddon = async () => {
|
||||
if (!addonDetails) return;
|
||||
if (!addonDetails || !addonUrl) return;
|
||||
|
||||
try {
|
||||
setInstalling(true);
|
||||
|
|
@ -130,28 +169,28 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleAddon = (addon: ExtendedManifest, enabled: boolean) => {
|
||||
// Logic to enable/disable an addon
|
||||
Alert.alert(
|
||||
enabled ? 'Disable Addon' : 'Enable Addon',
|
||||
`Are you sure you want to ${enabled ? 'disable' : 'enable'} ${addon.name}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: enabled ? 'Disable' : 'Enable',
|
||||
style: enabled ? 'destructive' : 'default',
|
||||
onPress: () => {
|
||||
// TODO: Implement actual toggle functionality
|
||||
Alert.alert('Success', `${addon.name} ${enabled ? 'disabled' : 'enabled'}`);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
const refreshAddons = async () => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonUp(addon.id)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
};
|
||||
|
||||
const moveAddonDown = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonDown(addon.id)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||
Alert.alert(
|
||||
'Uninstall',
|
||||
'Uninstall Addon',
|
||||
`Are you sure you want to uninstall ${addon.name}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
|
|
@ -160,26 +199,188 @@ const AddonsScreen = () => {
|
|||
style: 'destructive',
|
||||
onPress: () => {
|
||||
stremioService.removeAddon(addon.id);
|
||||
loadAddons();
|
||||
|
||||
// Remove from addons list
|
||||
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const renderAddonItem = ({ item }: { item: ExtendedManifest }) => {
|
||||
// Add function to handle configuration
|
||||
const handleConfigureAddon = (addon: ExtendedManifest, transportUrl?: string) => {
|
||||
// Try different ways to get the configuration URL
|
||||
let configUrl = '';
|
||||
|
||||
// Debug log the addon data to help troubleshoot
|
||||
logger.info(`Configure addon: ${addon.name}, ID: ${addon.id}`);
|
||||
if (transportUrl) {
|
||||
logger.info(`TransportUrl provided: ${transportUrl}`);
|
||||
}
|
||||
|
||||
// First check if the addon has a configurationURL directly
|
||||
if (addon.behaviorHints?.configurationURL) {
|
||||
configUrl = addon.behaviorHints.configurationURL;
|
||||
logger.info(`Using configurationURL from behaviorHints: ${configUrl}`);
|
||||
}
|
||||
// If a transport URL was provided directly (for community addons)
|
||||
else if (transportUrl) {
|
||||
// Remove any trailing filename like manifest.json
|
||||
const baseUrl = transportUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using transportUrl to create config URL: ${configUrl}`);
|
||||
}
|
||||
// If the addon has a url property (this is set during installation)
|
||||
else if (addon.url) {
|
||||
configUrl = `${addon.url}configure`;
|
||||
logger.info(`Using addon.url property: ${configUrl}`);
|
||||
}
|
||||
// For com.stremio.*.addon format (common format for installed addons)
|
||||
else if (addon.id && addon.id.match(/^com\.stremio\.(.*?)\.addon$/)) {
|
||||
// Extract the domain part
|
||||
const match = addon.id.match(/^com\.stremio\.(.*?)\.addon$/);
|
||||
if (match && match[1]) {
|
||||
// Construct URL from the domain part of the ID
|
||||
const addonName = match[1];
|
||||
// For torrentio specifically, use known URL
|
||||
if (addonName === 'torrentio') {
|
||||
configUrl = 'https://torrentio.strem.fun/configure';
|
||||
logger.info(`Special case for torrentio: ${configUrl}`);
|
||||
} else {
|
||||
// Try to construct a reasonable URL for other addons
|
||||
configUrl = `https://${addonName}.strem.fun/configure`;
|
||||
logger.info(`Constructed URL from addon name: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the ID is a URL, use that as the base (common for installed addons)
|
||||
else if (addon.id && addon.id.startsWith('http')) {
|
||||
// Get base URL from addon id (remove manifest.json or any trailing file)
|
||||
const baseUrl = addon.id.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using addon.id as HTTP URL: ${configUrl}`);
|
||||
}
|
||||
// If the ID uses stremio:// protocol but contains http URL (common format)
|
||||
else if (addon.id && (addon.id.includes('https://') || addon.id.includes('http://'))) {
|
||||
// Extract the HTTP URL using a more flexible regex
|
||||
const match = addon.id.match(/(https?:\/\/[^\/]+)(\/[^\s]*)?/);
|
||||
if (match) {
|
||||
// Use the domain and path if available, otherwise just domain with /configure
|
||||
const domain = match[1];
|
||||
const path = match[2] ? match[2].replace(/\/[^\/]+\.json$/, '/') : '/';
|
||||
configUrl = `${domain}${path}configure`;
|
||||
logger.info(`Extracted HTTP URL from stremio:// format: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for common addon format like stremio://addon.stremio.com/...
|
||||
if (!configUrl && addon.id && addon.id.startsWith('stremio://')) {
|
||||
// Try to convert stremio://domain.com/... to https://domain.com/...
|
||||
const domainMatch = addon.id.match(/stremio:\/\/([^\/]+)(\/[^\s]*)?/);
|
||||
if (domainMatch) {
|
||||
const domain = domainMatch[1];
|
||||
const path = domainMatch[2] ? domainMatch[2].replace(/\/[^\/]+\.json$/, '/') : '/';
|
||||
configUrl = `https://${domain}${path}configure`;
|
||||
logger.info(`Converted stremio:// protocol to https:// for config URL: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Use transport property if available (some addons include this)
|
||||
if (!configUrl && addon.transport && typeof addon.transport === 'string' && addon.transport.includes('http')) {
|
||||
const baseUrl = addon.transport.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using addon.transport for config URL: ${configUrl}`);
|
||||
}
|
||||
|
||||
// Get the URL from manifest's originalUrl if available
|
||||
if (!configUrl && (addon as any).originalUrl) {
|
||||
const baseUrl = (addon as any).originalUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using originalUrl property: ${configUrl}`);
|
||||
}
|
||||
|
||||
// If we couldn't determine a config URL, show an error
|
||||
if (!configUrl) {
|
||||
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
|
||||
Alert.alert(
|
||||
'Configuration Unavailable',
|
||||
'Could not determine configuration URL for this addon.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the URL being opened
|
||||
logger.info(`Opening configuration for addon: ${addon.name} at URL: ${configUrl}`);
|
||||
|
||||
// Check if the URL can be opened
|
||||
Linking.canOpenURL(configUrl).then(supported => {
|
||||
if (supported) {
|
||||
Linking.openURL(configUrl);
|
||||
} else {
|
||||
logger.error(`URL cannot be opened: ${configUrl}`);
|
||||
Alert.alert(
|
||||
'Cannot Open Configuration',
|
||||
`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
}).catch(err => {
|
||||
logger.error(`Error checking if URL can be opened: ${configUrl}`, err);
|
||||
Alert.alert('Error', 'Could not open configuration page.');
|
||||
});
|
||||
};
|
||||
|
||||
const toggleReorderMode = () => {
|
||||
setReorderMode(!reorderMode);
|
||||
};
|
||||
|
||||
const renderAddonItem = ({ item, index }: { item: ExtendedManifest, index: number }) => {
|
||||
const types = item.types || [];
|
||||
const description = item.description || '';
|
||||
// @ts-ignore - some addons might have logo property even though it's not in the type
|
||||
const logo = item.logo || null;
|
||||
// Check if addon is configurable
|
||||
const isConfigurable = item.behaviorHints?.configurable === true;
|
||||
|
||||
// Format the types into a simple category text
|
||||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'No categories';
|
||||
|
||||
const isFirstItem = index === 0;
|
||||
const isLastItem = index === addons.length - 1;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.addonItem}>
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonUp(item)}
|
||||
disabled={isFirstItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-upward"
|
||||
size={20}
|
||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonDown(item)}
|
||||
disabled={isLastItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-downward"
|
||||
size={20}
|
||||
color={isLastItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.addonHeader}>
|
||||
{logo ? (
|
||||
<ExpoImage
|
||||
|
|
@ -200,13 +401,30 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.addonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={true} // Default to enabled
|
||||
onValueChange={(value) => handleToggleAddon(item, !value)}
|
||||
trackColor={{ false: colors.elevation1, true: colors.primary }}
|
||||
thumbColor={colors.white}
|
||||
ios_backgroundColor={colors.elevation1}
|
||||
/>
|
||||
<View style={styles.addonActions}>
|
||||
{!reorderMode ? (
|
||||
<>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.priorityBadge}>
|
||||
<Text style={styles.priorityText}>#{index + 1}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
|
|
@ -216,6 +434,66 @@ const AddonsScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Function to render community addon items
|
||||
const renderCommunityAddonItem = ({ item }: { item: CommunityAddon }) => {
|
||||
const { manifest, transportUrl } = item;
|
||||
const types = manifest.types || [];
|
||||
const description = manifest.description || 'No description provided.';
|
||||
// @ts-ignore - logo might exist
|
||||
const logo = manifest.logo || null;
|
||||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General';
|
||||
// Check if addon is configurable
|
||||
const isConfigurable = manifest.behaviorHints?.configurable === true;
|
||||
|
||||
return (
|
||||
<View style={styles.communityAddonItem}>
|
||||
{logo ? (
|
||||
<ExpoImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.communityAddonIcon}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.communityAddonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.darkGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.communityAddonDetails}>
|
||||
<Text style={styles.communityAddonName}>{manifest.name}</Text>
|
||||
<Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text>
|
||||
<View style={styles.communityAddonMetaContainer}>
|
||||
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.communityAddonDot}>•</Text>
|
||||
<Text style={styles.communityAddonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActionButtons}>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
||||
<View style={styles.statsCard}>
|
||||
<Text style={styles.statsValue}>{value}</Text>
|
||||
|
|
@ -236,9 +514,48 @@ const AddonsScreen = () => {
|
|||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Reorder Mode Toggle Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
||||
onPress={toggleReorderMode}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="swap-vert"
|
||||
size={24}
|
||||
color={reorderMode ? colors.primary : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={refreshAddons}
|
||||
disabled={loading}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="refresh"
|
||||
size={24}
|
||||
color={loading ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.headerTitle}>Addons</Text>
|
||||
<Text style={styles.headerTitle}>
|
||||
Addons
|
||||
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
|
||||
</Text>
|
||||
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderInfoBanner}>
|
||||
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
||||
<Text style={styles.reorderInfoText}>
|
||||
Addons at the top have higher priority when loading content
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
|
|
@ -256,40 +573,44 @@ const AddonsScreen = () => {
|
|||
<View style={styles.statsContainer}>
|
||||
<StatsCard value={addons.length} label="Addons" />
|
||||
<View style={styles.statsDivider} />
|
||||
<StatsCard value={activeAddons} label="Active" />
|
||||
<StatsCard value={addons.length} label="Active" />
|
||||
<View style={styles.statsDivider} />
|
||||
<StatsCard value={catalogCount} label="Catalogs" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Add Addon Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>ADD NEW ADDON</Text>
|
||||
<View style={styles.addAddonContainer}>
|
||||
<TextInput
|
||||
style={styles.addonInput}
|
||||
placeholder="Addon URL"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
value={addonUrl}
|
||||
onChangeText={setAddonUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
onPress={handleAddAddon}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* Hide Add Addon Section in reorder mode */}
|
||||
{!reorderMode && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>ADD NEW ADDON</Text>
|
||||
<View style={styles.addAddonContainer}>
|
||||
<TextInput
|
||||
style={styles.addonInput}
|
||||
placeholder="Addon URL"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
value={addonUrl}
|
||||
onChangeText={setAddonUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
onPress={() => handleAddAddon()}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Installed Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>INSTALLED ADDONS</Text>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{reorderMode ? "DRAG ADDONS TO REORDER" : "INSTALLED ADDONS"}
|
||||
</Text>
|
||||
<View style={styles.addonList}>
|
||||
{addons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
|
|
@ -297,20 +618,103 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.emptyText}>No addons installed</Text>
|
||||
</View>
|
||||
) : (
|
||||
addons.map((addon, index) => {
|
||||
const isLast = index === addons.length - 1;
|
||||
return (
|
||||
<View
|
||||
key={addon.id}
|
||||
style={[
|
||||
styles.addonItem,
|
||||
{ marginBottom: isLast ? 32 : 16 }
|
||||
]}
|
||||
>
|
||||
{renderAddonItem({ item: addon })}
|
||||
addons.map((addon, index) => (
|
||||
<View
|
||||
key={addon.id}
|
||||
style={{ marginBottom: index === addons.length - 1 ? 32 : 0 }}
|
||||
>
|
||||
{renderAddonItem({ item: addon, index })}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Separator */}
|
||||
<View style={styles.sectionSeparator} />
|
||||
|
||||
{/* Community Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
|
||||
<View style={styles.addonList}>
|
||||
{communityLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
) : communityError ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="error-outline" size={32} color={colors.error} />
|
||||
<Text style={styles.emptyText}>{communityError}</Text>
|
||||
</View>
|
||||
) : communityAddons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
||||
<Text style={styles.emptyText}>No community addons available</Text>
|
||||
</View>
|
||||
) : (
|
||||
communityAddons.map((item, index) => (
|
||||
<View
|
||||
key={item.transportUrl}
|
||||
style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }}
|
||||
>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{item.manifest.logo ? (
|
||||
<ExpoImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
style={styles.addonIcon}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.addonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.mediumGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<Text style={styles.addonName}>{item.manifest.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{item.manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>
|
||||
{item.manifest.types && item.manifest.types.length > 0
|
||||
? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{item.manifest.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(item.transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
{item.manifest.description
|
||||
? (item.manifest.description.length > 100
|
||||
? item.manifest.description.substring(0, 100) + '...'
|
||||
: item.manifest.description)
|
||||
: 'No description provided.'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -440,9 +844,76 @@ const styles = StyleSheet.create({
|
|||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerButton: {
|
||||
padding: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
activeHeaderButton: {
|
||||
backgroundColor: 'rgba(45, 156, 219, 0.2)',
|
||||
borderRadius: 6,
|
||||
},
|
||||
reorderModeText: {
|
||||
color: colors.primary,
|
||||
fontSize: 18,
|
||||
fontWeight: '400',
|
||||
},
|
||||
reorderInfoBanner: {
|
||||
backgroundColor: 'rgba(45, 156, 219, 0.15)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
reorderInfoText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
marginLeft: 8,
|
||||
},
|
||||
reorderButtons: {
|
||||
position: 'absolute',
|
||||
left: -12,
|
||||
top: '50%',
|
||||
marginTop: -40,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
reorderButton: {
|
||||
backgroundColor: colors.elevation3,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginVertical: 4,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.5,
|
||||
backgroundColor: colors.elevation2,
|
||||
},
|
||||
priorityBadge: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
priorityText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -569,6 +1040,7 @@ const styles = StyleSheet.create({
|
|||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
marginBottom: 16,
|
||||
},
|
||||
addonHeader: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -748,12 +1220,119 @@ const styles = StyleSheet.create({
|
|||
marginRight: 8,
|
||||
},
|
||||
installButton: {
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: colors.success,
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalButtonText: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
addonActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 6,
|
||||
},
|
||||
configButton: {
|
||||
padding: 6,
|
||||
marginRight: 8,
|
||||
},
|
||||
communityAddonsList: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
communityAddonItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
marginBottom: 10,
|
||||
},
|
||||
communityAddonIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
},
|
||||
communityAddonIconPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
backgroundColor: colors.darkGray,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
communityAddonDetails: {
|
||||
flex: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
communityAddonName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 3,
|
||||
},
|
||||
communityAddonDesc: {
|
||||
fontSize: 13,
|
||||
color: colors.lightGray,
|
||||
marginBottom: 5,
|
||||
opacity: 0.9,
|
||||
},
|
||||
communityAddonMetaContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
opacity: 0.8,
|
||||
},
|
||||
communityAddonVersion: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
},
|
||||
communityAddonDot: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
communityAddonCategory: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
flexShrink: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 10,
|
||||
},
|
||||
sectionSeparator: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 20,
|
||||
},
|
||||
emptyMessage: {
|
||||
textAlign: 'center',
|
||||
color: colors.mediumGray,
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
errorMessage: {
|
||||
textAlign: 'center',
|
||||
color: colors.error,
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
loader: {
|
||||
marginTop: 30,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
addonActionButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default AddonsScreen;
|
||||
|
|
@ -20,6 +20,8 @@ import { colors } from '../styles';
|
|||
import { Image } from 'expo-image';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
|
||||
|
||||
type CatalogScreenProps = {
|
||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
||||
|
|
@ -44,16 +46,29 @@ const ITEM_MARGIN = SPACING.sm;
|
|||
const ITEM_WIDTH = (width - (SPACING.lg * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS;
|
||||
|
||||
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||
const { addonId, type, id, name, genreFilter } = route.params;
|
||||
const { addonId, type, id, name: originalName, genreFilter } = route.params;
|
||||
const [items, setItems] = useState<Meta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Force dark mode
|
||||
const [dataSource, setDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
|
||||
const isDarkMode = true;
|
||||
|
||||
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
|
||||
const displayName = getCustomName(addonId || '', type || '', id || '', originalName || '');
|
||||
|
||||
// Add effect to get data source preference when component mounts
|
||||
useEffect(() => {
|
||||
const getDataSourcePreference = async () => {
|
||||
const preference = await catalogService.getDataSourcePreference();
|
||||
setDataSource(preference);
|
||||
};
|
||||
|
||||
getDataSourcePreference();
|
||||
}, []);
|
||||
|
||||
const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => {
|
||||
try {
|
||||
if (shouldRefresh) {
|
||||
|
|
@ -64,6 +79,73 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
|
||||
setError(null);
|
||||
|
||||
// Process the genre filter - ignore "All" and clean up the value
|
||||
let effectiveGenreFilter = genreFilter;
|
||||
if (effectiveGenreFilter === 'All') {
|
||||
effectiveGenreFilter = undefined;
|
||||
logger.log('Genre "All" detected, removing genre filter');
|
||||
} else if (effectiveGenreFilter) {
|
||||
// Clean up the genre filter
|
||||
effectiveGenreFilter = effectiveGenreFilter.trim();
|
||||
logger.log(`Using cleaned genre filter: "${effectiveGenreFilter}"`);
|
||||
}
|
||||
|
||||
// Check if using TMDB as data source and not requesting a specific addon
|
||||
if (dataSource === DataSource.TMDB && !addonId) {
|
||||
logger.log('Using TMDB data source for CatalogScreen');
|
||||
try {
|
||||
const catalogs = await catalogService.getCatalogByType(type, effectiveGenreFilter);
|
||||
if (catalogs && catalogs.length > 0) {
|
||||
// Flatten all items from all catalogs
|
||||
const allItems: StreamingContent[] = [];
|
||||
catalogs.forEach(catalog => {
|
||||
allItems.push(...catalog.items);
|
||||
});
|
||||
|
||||
// Convert StreamingContent to Meta format
|
||||
const metaItems: Meta[] = allItems.map(item => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
poster: item.poster,
|
||||
background: item.banner,
|
||||
logo: item.logo,
|
||||
description: item.description,
|
||||
releaseInfo: item.year?.toString() || '',
|
||||
imdbRating: item.imdbRating,
|
||||
year: item.year,
|
||||
genres: item.genres || [],
|
||||
runtime: item.runtime,
|
||||
certification: item.certification,
|
||||
}));
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueItems = metaItems.filter((item, index, self) =>
|
||||
index === self.findIndex((t) => t.id === item.id)
|
||||
);
|
||||
|
||||
setItems(uniqueItems);
|
||||
setHasMore(false); // TMDB already returns a full set
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
return;
|
||||
} else {
|
||||
setError("No content found for the selected filters");
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get TMDB catalog:', error);
|
||||
setError('Failed to load content from TMDB');
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Use this flag to track if we found and processed any items
|
||||
let foundItems = false;
|
||||
let allItems: Meta[] = [];
|
||||
|
|
@ -80,7 +162,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
}
|
||||
|
||||
// Create filters array for genre filtering if provided
|
||||
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
|
||||
const filters = effectiveGenreFilter ? [{ title: 'genre', value: effectiveGenreFilter }] : [];
|
||||
|
||||
// Load items from the catalog
|
||||
const newItems = await stremioService.getCatalog(addon, type, id, pageNum, filters);
|
||||
|
|
@ -96,12 +178,15 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
} else {
|
||||
setItems(prev => [...prev, ...newItems]);
|
||||
}
|
||||
} else if (genreFilter) {
|
||||
} else if (effectiveGenreFilter) {
|
||||
// Get all addons that have catalogs of the specified type
|
||||
const typeManifests = manifests.filter(manifest =>
|
||||
manifest.catalogs && manifest.catalogs.some(catalog => catalog.type === type)
|
||||
);
|
||||
|
||||
// Add debug logging for genre filter
|
||||
logger.log(`Using genre filter: "${effectiveGenreFilter}" for type: ${type}`);
|
||||
|
||||
// For each addon, try to get content with the genre filter
|
||||
for (const manifest of typeManifests) {
|
||||
try {
|
||||
|
|
@ -111,12 +196,46 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
// For each catalog, try to get content
|
||||
for (const catalog of typeCatalogs) {
|
||||
try {
|
||||
const filters = [{ title: 'genre', value: genreFilter }];
|
||||
const filters = [{ title: 'genre', value: effectiveGenreFilter }];
|
||||
|
||||
// Debug logging for each catalog request
|
||||
logger.log(`Requesting from ${manifest.name}, catalog ${catalog.id} with genre "${effectiveGenreFilter}"`);
|
||||
|
||||
const catalogItems = await stremioService.getCatalog(manifest, type, catalog.id, pageNum, filters);
|
||||
|
||||
if (catalogItems && catalogItems.length > 0) {
|
||||
allItems = [...allItems, ...catalogItems];
|
||||
foundItems = true;
|
||||
// Log first few items' genres to debug
|
||||
const sampleItems = catalogItems.slice(0, 3);
|
||||
sampleItems.forEach(item => {
|
||||
logger.log(`Item "${item.name}" has genres: ${JSON.stringify(item.genres)}`);
|
||||
});
|
||||
|
||||
// Filter items client-side to ensure they contain the requested genre
|
||||
// Some addons might not properly filter by genre on the server
|
||||
let filteredItems = catalogItems;
|
||||
if (effectiveGenreFilter) {
|
||||
const normalizedGenreFilter = effectiveGenreFilter.toLowerCase().trim();
|
||||
|
||||
filteredItems = catalogItems.filter(item => {
|
||||
// Skip items without genres
|
||||
if (!item.genres || !Array.isArray(item.genres)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for genre match (exact or substring)
|
||||
return item.genres.some(genre => {
|
||||
const normalizedGenre = genre.toLowerCase().trim();
|
||||
return normalizedGenre === normalizedGenreFilter ||
|
||||
normalizedGenre.includes(normalizedGenreFilter) ||
|
||||
normalizedGenreFilter.includes(normalizedGenre);
|
||||
});
|
||||
});
|
||||
|
||||
logger.log(`Filtered ${catalogItems.length} items to ${filteredItems.length} matching genre "${effectiveGenreFilter}"`);
|
||||
}
|
||||
|
||||
allItems = [...allItems, ...filteredItems];
|
||||
foundItems = filteredItems.length > 0;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log(`Failed to load items from ${manifest.name} catalog ${catalog.id}:`, error);
|
||||
|
|
@ -160,7 +279,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [addonId, type, id, genreFilter]);
|
||||
}, [addonId, type, id, genreFilter, dataSource]);
|
||||
|
||||
useEffect(() => {
|
||||
loadItems(1);
|
||||
|
|
@ -246,7 +365,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
</View>
|
||||
);
|
||||
|
||||
if (loading && items.length === 0) {
|
||||
const isScreenLoading = loading || isLoadingCustomNames;
|
||||
|
||||
if (isScreenLoading && items.length === 0) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
|
@ -259,7 +380,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderLoadingState()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -278,7 +399,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderErrorState()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -296,7 +417,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
|
||||
{items.length > 0 ? (
|
||||
<FlatList
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ import {
|
|||
SafeAreaView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
Modal,
|
||||
TextInput,
|
||||
Pressable,
|
||||
Button,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -18,6 +23,7 @@ import { stremioService } from '../services/stremioService';
|
|||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { logger } from '../utils/logger';
|
||||
import { BlurView } from 'expo-blur';
|
||||
|
||||
interface CatalogSetting {
|
||||
addonId: string;
|
||||
|
|
@ -25,6 +31,7 @@ interface CatalogSetting {
|
|||
type: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
customName?: string;
|
||||
}
|
||||
|
||||
interface CatalogSettingsStorage {
|
||||
|
|
@ -42,6 +49,7 @@ interface GroupedCatalogs {
|
|||
}
|
||||
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
const CatalogSettingsScreen = () => {
|
||||
|
|
@ -52,6 +60,11 @@ const CatalogSettingsScreen = () => {
|
|||
const { refreshCatalogs } = useCatalogContext();
|
||||
const isDarkMode = true; // Force dark mode
|
||||
|
||||
// Modal State
|
||||
const [isRenameModalVisible, setIsRenameModalVisible] = useState(false);
|
||||
const [catalogToRename, setCatalogToRename] = useState<CatalogSetting | null>(null);
|
||||
const [currentRenameValue, setCurrentRenameValue] = useState('');
|
||||
|
||||
// Load saved settings and available catalogs
|
||||
const loadSettings = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -61,24 +74,22 @@ const CatalogSettingsScreen = () => {
|
|||
const addons = await stremioService.getInstalledAddonsAsync();
|
||||
const availableCatalogs: CatalogSetting[] = [];
|
||||
|
||||
// Get saved settings
|
||||
const savedSettings = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||
const savedCatalogs: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {};
|
||||
// Get saved enable/disable settings
|
||||
const savedSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||
const savedEnabledSettings: { [key: string]: boolean } = savedSettingsJson ? JSON.parse(savedSettingsJson) : {};
|
||||
|
||||
// Get saved custom names
|
||||
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
|
||||
const savedCustomNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
|
||||
|
||||
// Process each addon's catalogs
|
||||
addons.forEach(addon => {
|
||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
||||
// Create a map to store unique catalogs by their type and id
|
||||
const uniqueCatalogs = new Map<string, CatalogSetting>();
|
||||
|
||||
addon.catalogs.forEach(catalog => {
|
||||
// Create a unique key that includes addon id, type, and catalog id
|
||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||
|
||||
// Format catalog name
|
||||
let displayName = catalog.name || catalog.id;
|
||||
|
||||
// If catalog is a movie or series catalog, make that clear
|
||||
const catalogType = catalog.type === 'movie' ? 'Movies' : catalog.type === 'series' ? 'TV Shows' : catalog.type.charAt(0).toUpperCase() + catalog.type.slice(1);
|
||||
|
||||
uniqueCatalogs.set(settingKey, {
|
||||
|
|
@ -86,18 +97,17 @@ const CatalogSettingsScreen = () => {
|
|||
catalogId: catalog.id,
|
||||
type: catalog.type,
|
||||
name: displayName,
|
||||
enabled: savedCatalogs[settingKey] !== undefined ? savedCatalogs[settingKey] : true // Enable by default
|
||||
enabled: savedEnabledSettings[settingKey] !== undefined ? savedEnabledSettings[settingKey] : true,
|
||||
customName: savedCustomNames[settingKey]
|
||||
});
|
||||
});
|
||||
|
||||
// Add unique catalogs to the available catalogs array
|
||||
availableCatalogs.push(...uniqueCatalogs.values());
|
||||
}
|
||||
});
|
||||
|
||||
// Group settings by addon name
|
||||
const grouped: GroupedCatalogs = {};
|
||||
|
||||
availableCatalogs.forEach(setting => {
|
||||
const addon = addons.find(a => a.id === setting.addonId);
|
||||
if (!addon) return;
|
||||
|
|
@ -106,7 +116,7 @@ const CatalogSettingsScreen = () => {
|
|||
grouped[setting.addonId] = {
|
||||
name: addon.name,
|
||||
catalogs: [],
|
||||
expanded: true, // Start expanded
|
||||
expanded: true,
|
||||
enabledCount: 0
|
||||
};
|
||||
}
|
||||
|
|
@ -126,8 +136,8 @@ const CatalogSettingsScreen = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Save settings when they change
|
||||
const saveSettings = async (newSettings: CatalogSetting[]) => {
|
||||
// Save settings when they change (ENABLE/DISABLE ONLY)
|
||||
const saveEnabledSettings = async (newSettings: CatalogSetting[]) => {
|
||||
try {
|
||||
const settingsObj: CatalogSettingsStorage = {
|
||||
_lastUpdate: Date.now()
|
||||
|
|
@ -139,11 +149,11 @@ const CatalogSettingsScreen = () => {
|
|||
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
||||
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
||||
} catch (error) {
|
||||
logger.error('Failed to save catalog settings:', error);
|
||||
logger.error('Failed to save catalog enabled settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle individual catalog
|
||||
// Toggle individual catalog enabled state
|
||||
const toggleCatalog = (addonId: string, index: number) => {
|
||||
const newSettings = [...settings];
|
||||
const catalogsForAddon = groupedSettings[addonId].catalogs;
|
||||
|
|
@ -154,7 +164,6 @@ const CatalogSettingsScreen = () => {
|
|||
enabled: !setting.enabled
|
||||
};
|
||||
|
||||
// Update the setting in the flat list
|
||||
const flatIndex = newSettings.findIndex(s =>
|
||||
s.addonId === setting.addonId &&
|
||||
s.type === setting.type &&
|
||||
|
|
@ -165,14 +174,13 @@ const CatalogSettingsScreen = () => {
|
|||
newSettings[flatIndex] = updatedSetting;
|
||||
}
|
||||
|
||||
// Update the grouped settings
|
||||
const newGroupedSettings = { ...groupedSettings };
|
||||
newGroupedSettings[addonId].catalogs[index] = updatedSetting;
|
||||
newGroupedSettings[addonId].enabledCount += updatedSetting.enabled ? 1 : -1;
|
||||
|
||||
setSettings(newSettings);
|
||||
setGroupedSettings(newGroupedSettings);
|
||||
saveSettings(newSettings);
|
||||
saveEnabledSettings(newSettings); // Use specific save function
|
||||
};
|
||||
|
||||
// Toggle expansion of a group
|
||||
|
|
@ -186,6 +194,47 @@ const CatalogSettingsScreen = () => {
|
|||
}));
|
||||
};
|
||||
|
||||
// Handle long press on catalog item
|
||||
const handleLongPress = (setting: CatalogSetting) => {
|
||||
setCatalogToRename(setting);
|
||||
setCurrentRenameValue(setting.customName || setting.name);
|
||||
setIsRenameModalVisible(true);
|
||||
};
|
||||
|
||||
// Handle saving the renamed catalog
|
||||
const handleSaveRename = async () => {
|
||||
if (!catalogToRename || !currentRenameValue) return;
|
||||
|
||||
const settingKey = `${catalogToRename.addonId}:${catalogToRename.type}:${catalogToRename.catalogId}`;
|
||||
|
||||
try {
|
||||
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
|
||||
const customNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
|
||||
|
||||
const trimmedNewName = currentRenameValue.trim();
|
||||
|
||||
if (trimmedNewName === catalogToRename.name || trimmedNewName === '') {
|
||||
delete customNames[settingKey];
|
||||
} else {
|
||||
customNames[settingKey] = trimmedNewName;
|
||||
}
|
||||
|
||||
await AsyncStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames));
|
||||
|
||||
// --- Reload settings to reflect the change ---
|
||||
await loadSettings();
|
||||
// --- No need to manually update local state anymore ---
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to save custom catalog name:', error);
|
||||
Alert.alert('Error', 'Could not save the custom name.'); // Inform user
|
||||
} finally {
|
||||
setIsRenameModalVisible(false);
|
||||
setCatalogToRename(null);
|
||||
setCurrentRenameValue('');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
|
@ -252,10 +301,17 @@ const CatalogSettingsScreen = () => {
|
|||
</TouchableOpacity>
|
||||
|
||||
{group.expanded && group.catalogs.map((setting, index) => (
|
||||
<View key={`${setting.addonId}:${setting.type}:${setting.catalogId}`} style={styles.catalogItem}>
|
||||
<Pressable
|
||||
key={`${setting.addonId}:${setting.type}:${setting.catalogId}`}
|
||||
onLongPress={() => handleLongPress(setting)} // Added long press handler
|
||||
style={({ pressed }) => [
|
||||
styles.catalogItem,
|
||||
pressed && styles.catalogItemPressed, // Optional pressed style
|
||||
]}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={styles.catalogName}>
|
||||
{setting.name}
|
||||
{setting.customName || setting.name} {/* Display custom or default name */}
|
||||
</Text>
|
||||
<Text style={styles.catalogType}>
|
||||
{setting.type.charAt(0).toUpperCase() + setting.type.slice(1)}
|
||||
|
|
@ -268,26 +324,68 @@ const CatalogSettingsScreen = () => {
|
|||
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
||||
ios_backgroundColor="#505050"
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={styles.addonSection}>
|
||||
<Text style={styles.addonTitle}>ORGANIZATION</Text>
|
||||
<View style={styles.card}>
|
||||
<TouchableOpacity style={styles.organizationItem}>
|
||||
<Text style={styles.organizationItemText}>Reorder Sections</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.organizationItem}>
|
||||
<Text style={styles.organizationItemText}>Customize Names</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Rename Modal */}
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={isRenameModalVisible}
|
||||
onRequestClose={() => {
|
||||
setIsRenameModalVisible(false);
|
||||
setCatalogToRename(null);
|
||||
}}
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setIsRenameModalVisible(false)}>
|
||||
<BlurView
|
||||
style={styles.modalContent}
|
||||
intensity={90}
|
||||
tint="default"
|
||||
>
|
||||
<Pressable onPress={(e) => e.stopPropagation()}>
|
||||
<Text style={styles.modalTitle}>Rename Catalog</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
value={currentRenameValue}
|
||||
onChangeText={setCurrentRenameValue}
|
||||
placeholder="Enter new catalog name"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
|
||||
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setIsRenameModalVisible(false)}>
|
||||
<Pressable style={styles.modalContent} onPress={(e) => e.stopPropagation()}>
|
||||
<Text style={styles.modalTitle}>Rename Catalog</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
value={currentRenameValue}
|
||||
onChangeText={setCurrentRenameValue}
|
||||
placeholder="Enter new catalog name"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
|
||||
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
|
@ -385,9 +483,14 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
// Ensure last item doesn't have border if needed (check logic)
|
||||
},
|
||||
catalogItemPressed: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)', // Subtle feedback for press
|
||||
},
|
||||
catalogInfo: {
|
||||
flex: 1,
|
||||
marginRight: 8, // Add space before switch
|
||||
},
|
||||
catalogName: {
|
||||
fontSize: 15,
|
||||
|
|
@ -398,18 +501,47 @@ const styles = StyleSheet.create({
|
|||
fontSize: 13,
|
||||
color: colors.mediumGray,
|
||||
},
|
||||
organizationItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
// Modal Styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
},
|
||||
organizationItemText: {
|
||||
fontSize: 17,
|
||||
modalContent: {
|
||||
backgroundColor: Platform.OS === 'ios' ? undefined : colors.elevation3,
|
||||
borderRadius: 14,
|
||||
padding: 20,
|
||||
width: '85%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
elevation: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalInput: {
|
||||
backgroundColor: colors.elevation1, // Darker input background
|
||||
color: colors.white,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around', // Adjust as needed (e.g., 'flex-end')
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,6 @@ const CatalogSection = React.memo(({
|
|||
const { width } = Dimensions.get('window');
|
||||
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
||||
|
||||
// Only display the first 3 items in the row
|
||||
const displayItems = useMemo(() =>
|
||||
catalog.items.slice(0, 3),
|
||||
[catalog.items]
|
||||
|
|
@ -207,13 +206,31 @@ const CatalogSection = React.memo(({
|
|||
), [handleContentPress]);
|
||||
|
||||
const handleSeeMorePress = useCallback(() => {
|
||||
// Get addon/catalog info from the first item (assuming homogeneity)
|
||||
const firstItem = catalog.items[0];
|
||||
if (!firstItem) return; // Should not happen if section exists
|
||||
|
||||
// We need addonId and catalogId. These aren't directly on StreamingContent.
|
||||
// We might need to fetch this or adjust the GenreCatalog structure.
|
||||
// FOR NOW: Assuming CatalogScreen can handle potentially missing addonId/catalogId
|
||||
// OR: We could pass the *genre* as the name and let CatalogScreen figure it out?
|
||||
// Let's pass the necessary info if available, assuming StreamingContent might have it
|
||||
// (Requires checking StreamingContent interface or how it's populated)
|
||||
|
||||
// --- TEMPORARY/PLACEHOLDER ---
|
||||
// Ideally, GenreCatalog should contain addonId/catalogId for the group.
|
||||
// If not, CatalogScreen needs modification or we fetch IDs here.
|
||||
// Let's stick to passing genre and type for now, CatalogScreen logic might suffice?
|
||||
navigation.navigate('Catalog', {
|
||||
id: 'discover',
|
||||
// Don't pass an addonId since we want to filter by genre across all addons
|
||||
id: catalog.genre,
|
||||
type: selectedCategory.type,
|
||||
name: `${catalog.genre} ${selectedCategory.name}`,
|
||||
genreFilter: catalog.genre
|
||||
genreFilter: catalog.genre // This will trigger the genre-based filtering logic in CatalogScreen
|
||||
});
|
||||
}, [navigation, selectedCategory, catalog.genre]);
|
||||
// --- END TEMPORARY ---
|
||||
|
||||
}, [navigation, selectedCategory, catalog.genre, catalog.items]);
|
||||
|
||||
const keyExtractor = useCallback((item: StreamingContent) => item.id, []);
|
||||
const ItemSeparator = useCallback(() => <View style={{ width: 16 }} />, []);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles/colors';
|
||||
import { catalogService, StreamingAddon } from '../services/catalogService';
|
||||
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
||||
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
||||
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigation.goBack();
|
||||
|
|
@ -125,7 +127,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
{loading || isLoadingCustomNames ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
|
|
@ -175,30 +177,35 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
styles.catalogsContainer,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
|
||||
]}>
|
||||
{addonCatalogs.map(catalog => (
|
||||
<TouchableOpacity
|
||||
key={catalog.id}
|
||||
style={[
|
||||
styles.catalogItem,
|
||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||
]}
|
||||
onPress={() => toggleCatalog(catalog.id)}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
{catalog.name}
|
||||
</Text>
|
||||
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialIcons
|
||||
name={selectedCatalogs.includes(catalog.id) ? "check-box" : "check-box-outline-blank"}
|
||||
size={24}
|
||||
color={selectedCatalogs.includes(catalog.id) ? colors.primary : isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{addonCatalogs.map(catalog => {
|
||||
const [addonId, type, catalogId] = catalog.id.split(':');
|
||||
const displayName = getCustomName(addonId, type, catalogId, catalog.name);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={catalog.id}
|
||||
style={[
|
||||
styles.catalogItem,
|
||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||
]}
|
||||
onPress={() => toggleCatalog(catalog.id)}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialIcons
|
||||
name={selectedCatalogs.includes(catalog.id) ? "check-box" : "check-box-outline-blank"}
|
||||
size={24}
|
||||
color={selectedCatalogs.includes(catalog.id) ? colors.primary : isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ const ActionButtons = React.memo(({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="star-rate" size={24} color="#fff" />
|
||||
<MaterialIcons name="assessment" size={24} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
|
@ -214,6 +214,7 @@ const MetadataScreen = () => {
|
|||
recommendations,
|
||||
loadingRecommendations,
|
||||
setMetadata,
|
||||
imdbId,
|
||||
} = useMetadata({ id, type });
|
||||
|
||||
// Get genres from context
|
||||
|
|
@ -258,7 +259,7 @@ const MetadataScreen = () => {
|
|||
|
||||
// Fetch logo immediately for TMDB content
|
||||
useEffect(() => {
|
||||
if (metadata && id.startsWith('tmdb:')) {
|
||||
if (metadata && id.startsWith('tmdb:') && !metadata.logo) {
|
||||
const fetchLogo = async () => {
|
||||
try {
|
||||
const tmdbId = id.split(':')[1];
|
||||
|
|
@ -984,9 +985,9 @@ const MetadataScreen = () => {
|
|||
</View>
|
||||
|
||||
{/* Add RatingsSection right under the main metadata */}
|
||||
{id && (
|
||||
{imdbId && (
|
||||
<RatingsSection
|
||||
imdbId={id}
|
||||
imdbId={imdbId}
|
||||
type={type === 'series' ? 'show' : 'movie'}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
313
src/screens/PlayerSettingsScreen.tsx
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
Platform,
|
||||
useColorScheme,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useSettings, AppSettings } from '../hooks/useSettings';
|
||||
import { colors } from '../styles/colors';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
interface SettingItemProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
isDarkMode: boolean;
|
||||
isSelected: boolean;
|
||||
onPress: () => void;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
const SettingItem: React.FC<SettingItemProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
isDarkMode,
|
||||
isSelected,
|
||||
onPress,
|
||||
isLast,
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
style={[
|
||||
styles.settingItem,
|
||||
!isLast && styles.settingItemBorder,
|
||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' },
|
||||
]}
|
||||
>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name={icon}
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settingText}>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark },
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text
|
||||
style={[
|
||||
styles.settingDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark },
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{isSelected && (
|
||||
<MaterialIcons
|
||||
name="check"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
style={styles.checkIcon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const PlayerSettingsScreen: React.FC = () => {
|
||||
const { settings, updateSetting } = useSettings();
|
||||
const systemColorScheme = useColorScheme();
|
||||
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
|
||||
const navigation = useNavigation();
|
||||
|
||||
const playerOptions = [
|
||||
{
|
||||
id: 'internal',
|
||||
title: 'Built-in Player',
|
||||
description: 'Use the app\'s default video player',
|
||||
icon: 'play-circle-outline',
|
||||
},
|
||||
...(Platform.OS === 'ios' ? [
|
||||
{
|
||||
id: 'vlc',
|
||||
title: 'VLC',
|
||||
description: 'Open streams in VLC media player',
|
||||
icon: 'video-library',
|
||||
},
|
||||
{
|
||||
id: 'infuse',
|
||||
title: 'Infuse',
|
||||
description: 'Open streams in Infuse player',
|
||||
icon: 'smart-display',
|
||||
},
|
||||
{
|
||||
id: 'outplayer',
|
||||
title: 'OutPlayer',
|
||||
description: 'Open streams in OutPlayer',
|
||||
icon: 'slideshow',
|
||||
},
|
||||
{
|
||||
id: 'vidhub',
|
||||
title: 'VidHub',
|
||||
description: 'Open streams in VidHub player',
|
||||
icon: 'ondemand-video',
|
||||
},
|
||||
] : [
|
||||
{
|
||||
id: 'external',
|
||||
title: 'External Player',
|
||||
description: 'Open streams in your preferred video player',
|
||||
icon: 'open-in-new',
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
const handleBack = () => {
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' },
|
||||
]}
|
||||
>
|
||||
<StatusBar
|
||||
translucent
|
||||
backgroundColor="transparent"
|
||||
barStyle={isDarkMode ? "light-content" : "dark-content"}
|
||||
/>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text
|
||||
style={[
|
||||
styles.headerTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark },
|
||||
]}
|
||||
>
|
||||
Video Player
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark },
|
||||
]}
|
||||
>
|
||||
PLAYER SELECTION
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: isDarkMode
|
||||
? colors.elevation2
|
||||
: colors.white,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{playerOptions.map((option, index) => (
|
||||
<SettingItem
|
||||
key={option.id}
|
||||
title={option.title}
|
||||
description={option.description}
|
||||
icon={option.icon}
|
||||
isDarkMode={isDarkMode}
|
||||
isSelected={
|
||||
Platform.OS === 'ios'
|
||||
? settings.preferredPlayer === option.id
|
||||
: settings.useExternalPlayer === (option.id === 'external')
|
||||
}
|
||||
onPress={() => {
|
||||
if (Platform.OS === 'ios') {
|
||||
updateSetting('preferredPlayer', option.id as AppSettings['preferredPlayer'], false);
|
||||
} else {
|
||||
updateSetting('useExternalPlayer', option.id === 'external', false);
|
||||
}
|
||||
}}
|
||||
isLast={index === playerOptions.length - 1}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
marginRight: 16,
|
||||
borderRadius: 20,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 24,
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 4,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 24,
|
||||
shadowColor: 'rgba(0,0,0,0.1)',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
settingItem: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
settingItemBorder: {
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
settingContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
settingIconContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
settingText: {
|
||||
flex: 1,
|
||||
},
|
||||
settingTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 2,
|
||||
},
|
||||
settingDescription: {
|
||||
fontSize: 14,
|
||||
marginTop: 2,
|
||||
},
|
||||
checkIcon: {
|
||||
marginLeft: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default PlayerSettingsScreen;
|
||||
|
|
@ -12,34 +12,48 @@ import {
|
|||
Alert,
|
||||
Platform,
|
||||
Dimensions,
|
||||
Pressable
|
||||
Image
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { stremioService } from '../services/stremioService';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { catalogService, DataSource } from '../services/catalogService';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
// Card component for iOS Fluent design style
|
||||
// Card component with modern style
|
||||
interface SettingsCardProps {
|
||||
children: React.ReactNode;
|
||||
isDarkMode: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode }) => (
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
]}>
|
||||
{children}
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => (
|
||||
<View style={[styles.cardContainer]}>
|
||||
{title && (
|
||||
<Text style={[
|
||||
styles.cardTitle,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
{title.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -51,6 +65,7 @@ interface SettingItemProps {
|
|||
isLast?: boolean;
|
||||
onPress?: () => void;
|
||||
isDarkMode: boolean;
|
||||
badge?: string | number;
|
||||
}
|
||||
|
||||
const SettingItem: React.FC<SettingItemProps> = ({
|
||||
|
|
@ -60,7 +75,8 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
renderControl,
|
||||
isLast = false,
|
||||
onPress,
|
||||
isDarkMode
|
||||
isDarkMode,
|
||||
badge
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
@ -72,11 +88,14 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||
]}
|
||||
>
|
||||
<View style={styles.settingIconContainer}>
|
||||
<MaterialIcons name={icon} size={22} color={colors.primary} />
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
|
||||
]}>
|
||||
<MaterialIcons name={icon} size={20} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTitleRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
{title}
|
||||
</Text>
|
||||
|
|
@ -86,6 +105,11 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{badge && (
|
||||
<View style={[styles.badge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.badgeText}>{badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.settingControl}>
|
||||
{renderControl()}
|
||||
|
|
@ -94,28 +118,19 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const SectionHeader: React.FC<{ title: string; isDarkMode: boolean }> = ({ title, isDarkMode }) => (
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[
|
||||
styles.sectionHeaderText,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const SettingsScreen: React.FC = () => {
|
||||
const { settings, updateSetting } = useSettings();
|
||||
const systemColorScheme = useColorScheme();
|
||||
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { lastUpdate } = useCatalogContext();
|
||||
const { isAuthenticated, userProfile } = useTraktContext();
|
||||
|
||||
// States for dynamic content
|
||||
const [addonCount, setAddonCount] = useState<number>(0);
|
||||
const [catalogCount, setCatalogCount] = useState<number>(0);
|
||||
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
|
||||
const [discoverDataSource, setDiscoverDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -149,6 +164,10 @@ const SettingsScreen: React.FC = () => {
|
|||
// Check MDBList API key status
|
||||
const mdblistKey = await AsyncStorage.getItem('mdblist_api_key');
|
||||
setMdblistKeySet(!!mdblistKey);
|
||||
|
||||
// Get discover data source preference
|
||||
const dataSource = await catalogService.getDataSourcePreference();
|
||||
setDiscoverDataSource(dataSource);
|
||||
} catch (error) {
|
||||
console.error('Error loading settings data:', error);
|
||||
}
|
||||
|
|
@ -200,11 +219,18 @@ const SettingsScreen: React.FC = () => {
|
|||
const ChevronRight = () => (
|
||||
<MaterialIcons
|
||||
name="chevron-right"
|
||||
size={24}
|
||||
size={22}
|
||||
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
|
||||
/>
|
||||
);
|
||||
|
||||
// Handle data source change
|
||||
const handleDiscoverDataSourceChange = useCallback(async (value: string) => {
|
||||
const dataSource = value as DataSource;
|
||||
setDiscoverDataSource(dataSource);
|
||||
await catalogService.setDataSourcePreference(dataSource);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[
|
||||
styles.container,
|
||||
|
|
@ -215,68 +241,77 @@ const SettingsScreen: React.FC = () => {
|
|||
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
Settings
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
|
||||
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<SectionHeader title="USER & ACCOUNT" isDarkMode={isDarkMode} />
|
||||
<SettingsCard isDarkMode={isDarkMode}>
|
||||
<SettingsCard isDarkMode={isDarkMode} title="User & Account">
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description="Not Connected"
|
||||
description={isAuthenticated ? `Connected as ${userProfile?.username || 'User'}` : "Not Connected"}
|
||||
icon="person"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => Alert.alert('Trakt', 'Trakt integration coming soon')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="iCloud Sync"
|
||||
description="Enabled"
|
||||
icon="cloud"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SectionHeader title="CONTENT" isDarkMode={isDarkMode} />
|
||||
<SettingsCard isDarkMode={isDarkMode}>
|
||||
<SettingsCard isDarkMode={isDarkMode} title="Features">
|
||||
<SettingItem
|
||||
title="Calendar"
|
||||
description="Manage your show calendar settings"
|
||||
icon="calendar-today"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('Calendar')}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Notifications"
|
||||
description="Configure episode notifications and reminders"
|
||||
icon="notifications"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('NotificationSettings')}
|
||||
isDarkMode={isDarkMode}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard isDarkMode={isDarkMode} title="Content">
|
||||
<SettingItem
|
||||
title="Addons"
|
||||
description={addonCount + " installed"}
|
||||
description="Manage your installed addons"
|
||||
icon="extension"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
badge={addonCount}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Catalogs"
|
||||
description={`${catalogCount} ${catalogCount === 1 ? 'catalog' : 'catalogs'} enabled`}
|
||||
description="Configure content sources"
|
||||
icon="view-list"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('CatalogSettings')}
|
||||
badge={catalogCount}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Home Screen"
|
||||
description="Customize home layout and content"
|
||||
description="Customize layout and content"
|
||||
icon="home"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('HomeScreenSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Folders"
|
||||
description="0 created"
|
||||
icon="folder"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Ratings Source"
|
||||
description={mdblistKeySet ? "MDBList API Configured" : "MDBList API Not Set"}
|
||||
description={mdblistKeySet ? "MDBList API Configured" : "Configure MDBList API"}
|
||||
icon="info-outline"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
|
|
@ -289,41 +324,71 @@ const SettingsScreen: React.FC = () => {
|
|||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TMDBSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Resource Filters"
|
||||
icon="tune"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
/>
|
||||
<SettingItem
|
||||
title="AI Features"
|
||||
description="Not Connected"
|
||||
icon="auto-awesome"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SectionHeader title="PLAYBACK" isDarkMode={isDarkMode} />
|
||||
<SettingsCard isDarkMode={isDarkMode}>
|
||||
<SettingsCard isDarkMode={isDarkMode} title="Playback">
|
||||
<SettingItem
|
||||
title="Video Player"
|
||||
description="Infuse"
|
||||
description={Platform.OS === 'ios'
|
||||
? (settings.preferredPlayer === 'internal'
|
||||
? 'Built-in Player'
|
||||
: settings.preferredPlayer
|
||||
? settings.preferredPlayer.toUpperCase()
|
||||
: 'Built-in Player')
|
||||
: (settings.useExternalPlayer ? 'External Player' : 'Built-in Player')
|
||||
}
|
||||
icon="play-arrow"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Auto-Filtering"
|
||||
description="Disabled"
|
||||
icon="tune"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('PlayerSettings')}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard isDarkMode={isDarkMode} title="Discover">
|
||||
<SettingItem
|
||||
title="Content Source"
|
||||
description="Choose where to get content for the Discover screen"
|
||||
icon="explore"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={() => (
|
||||
<View style={styles.selectorContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
|
||||
]}
|
||||
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
|
||||
]}>Addons</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
|
||||
]}
|
||||
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
|
||||
]}>TMDB</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<View style={styles.versionContainer}>
|
||||
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
|
||||
Version 1.0.0
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -337,82 +402,150 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 8,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 34,
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
resetButton: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
resetButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 32,
|
||||
},
|
||||
sectionHeader: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 8,
|
||||
cardContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionHeaderText: {
|
||||
fontSize: 12,
|
||||
cardTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.8,
|
||||
marginLeft: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
card: {
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
elevation: 3,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 0.5,
|
||||
minHeight: 44,
|
||||
minHeight: 58,
|
||||
},
|
||||
settingItemBorder: {
|
||||
// Border styling handled directly in the component with borderBottomWidth
|
||||
},
|
||||
settingIconContainer: {
|
||||
marginRight: 12,
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginRight: 16,
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
settingContent: {
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
settingTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
settingTitleRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
settingTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '400',
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 3,
|
||||
},
|
||||
settingDescription: {
|
||||
fontSize: 14,
|
||||
opacity: 0.7,
|
||||
textAlign: 'right',
|
||||
flexShrink: 1,
|
||||
maxWidth: '60%',
|
||||
opacity: 0.8,
|
||||
},
|
||||
settingControl: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 8,
|
||||
paddingLeft: 12,
|
||||
},
|
||||
badge: {
|
||||
height: 22,
|
||||
minWidth: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 6,
|
||||
marginRight: 8,
|
||||
},
|
||||
badgeText: {
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
versionContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
versionText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
pickerContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
},
|
||||
selectorContainer: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
height: 36,
|
||||
width: 160,
|
||||
marginRight: 8,
|
||||
},
|
||||
selectorButton: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
selectorButtonActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
selectorText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: colors.mediumEmphasis,
|
||||
},
|
||||
selectorTextActive: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { colors } from '../styles';
|
||||
import { TMDBService, TMDBShow as Show, TMDBSeason, TMDBEpisode } from '../services/tmdbService';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
|
|
@ -367,7 +368,14 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.black }]}>
|
||||
<View style={[styles.container, { backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.black }]}>
|
||||
{Platform.OS === 'ios' && (
|
||||
<BlurView
|
||||
style={StyleSheet.absoluteFill}
|
||||
tint="dark"
|
||||
intensity={60}
|
||||
/>
|
||||
)}
|
||||
<StatusBar
|
||||
translucent
|
||||
backgroundColor="transparent"
|
||||
|
|
@ -528,7 +536,6 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
|
|
@ -548,7 +555,7 @@ const styles = StyleSheet.create({
|
|||
showInfo: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 12,
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
},
|
||||
|
|
@ -641,7 +648,7 @@ const styles = StyleSheet.create({
|
|||
lineHeight: 16,
|
||||
},
|
||||
legend: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
marginBottom: 12,
|
||||
|
|
@ -693,7 +700,7 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
},
|
||||
ratingsGrid: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
ScrollView,
|
||||
StatusBar,
|
||||
Alert,
|
||||
Dimensions
|
||||
Dimensions,
|
||||
Linking
|
||||
} from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
|
|
@ -343,6 +344,22 @@ export const StreamsScreen = () => {
|
|||
);
|
||||
}, [selectedEpisode, groupedEpisodes, id]);
|
||||
|
||||
const navigateToPlayer = useCallback((stream: Stream) => {
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode]);
|
||||
|
||||
// Update handleStreamPress
|
||||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||
try {
|
||||
|
|
@ -350,63 +367,164 @@ export const StreamsScreen = () => {
|
|||
logger.log('handleStreamPress called with stream:', {
|
||||
url: stream.url,
|
||||
behaviorHints: stream.behaviorHints,
|
||||
useExternalPlayer: settings.useExternalPlayer
|
||||
useExternalPlayer: settings.useExternalPlayer,
|
||||
preferredPlayer: settings.preferredPlayer
|
||||
});
|
||||
|
||||
// Check if external player is enabled in settings
|
||||
if (settings.useExternalPlayer) {
|
||||
logger.log('Using external player for URL:', stream.url);
|
||||
// Use VideoPlayerService to launch external player
|
||||
const videoPlayerService = VideoPlayerService;
|
||||
const launched = await videoPlayerService.playVideo(stream.url, {
|
||||
useExternalPlayer: true,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined,
|
||||
releaseDate: metadata?.year?.toString(),
|
||||
});
|
||||
|
||||
if (!launched) {
|
||||
logger.log('External player launch failed, falling back to built-in player');
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
// For iOS, try to open with the preferred external player
|
||||
if (Platform.OS === 'ios' && settings.preferredPlayer !== 'internal') {
|
||||
try {
|
||||
// Format the URL for the selected player
|
||||
const streamUrl = encodeURIComponent(stream.url);
|
||||
let externalPlayerUrls: string[] = [];
|
||||
|
||||
// Configure URL formats based on the selected player
|
||||
switch (settings.preferredPlayer) {
|
||||
case 'vlc':
|
||||
externalPlayerUrls = [
|
||||
`vlc://${stream.url}`,
|
||||
`vlc-x-callback://x-callback-url/stream?url=${streamUrl}`,
|
||||
`vlc://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'outplayer':
|
||||
externalPlayerUrls = [
|
||||
`outplayer://${stream.url}`,
|
||||
`outplayer://${streamUrl}`,
|
||||
`outplayer://play?url=${streamUrl}`,
|
||||
`outplayer://stream?url=${streamUrl}`,
|
||||
`outplayer://play/browser?url=${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'infuse':
|
||||
externalPlayerUrls = [
|
||||
`infuse://x-callback-url/play?url=${streamUrl}`,
|
||||
`infuse://play?url=${streamUrl}`,
|
||||
`infuse://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'vidhub':
|
||||
externalPlayerUrls = [
|
||||
`vidhub://play?url=${streamUrl}`,
|
||||
`vidhub://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
// If no matching player or the setting is somehow invalid, use internal player
|
||||
navigateToPlayer(stream);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Attempting to open stream in ${settings.preferredPlayer}`);
|
||||
|
||||
// Try each URL format in sequence
|
||||
const tryNextUrl = (index: number) => {
|
||||
if (index >= externalPlayerUrls.length) {
|
||||
console.log(`All ${settings.preferredPlayer} formats failed, falling back to direct URL`);
|
||||
// Try direct URL as last resort
|
||||
Linking.openURL(stream.url)
|
||||
.then(() => console.log('Opened with direct URL'))
|
||||
.catch(() => {
|
||||
console.log('Direct URL failed, falling back to built-in player');
|
||||
navigateToPlayer(stream);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const url = externalPlayerUrls[index];
|
||||
console.log(`Trying ${settings.preferredPlayer} URL format ${index + 1}: ${url}`);
|
||||
|
||||
Linking.openURL(url)
|
||||
.then(() => console.log(`Successfully opened stream with ${settings.preferredPlayer} format ${index + 1}`))
|
||||
.catch(err => {
|
||||
console.log(`Format ${index + 1} failed: ${err.message}`, err);
|
||||
tryNextUrl(index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
// Start with the first URL format
|
||||
tryNextUrl(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error with ${settings.preferredPlayer}:`, error);
|
||||
// Fallback to the built-in player
|
||||
navigateToPlayer(stream);
|
||||
}
|
||||
} else {
|
||||
// Use built-in player
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
}
|
||||
// For Android with external player preference
|
||||
else if (Platform.OS === 'android' && settings.useExternalPlayer) {
|
||||
try {
|
||||
console.log('Opening stream with Android native app chooser');
|
||||
|
||||
// For Android, determine if the URL is a direct http/https URL or a magnet link
|
||||
const isMagnet = stream.url.startsWith('magnet:');
|
||||
|
||||
if (isMagnet) {
|
||||
// For magnet links, open directly which will trigger the torrent app chooser
|
||||
console.log('Opening magnet link directly');
|
||||
Linking.openURL(stream.url)
|
||||
.then(() => console.log('Successfully opened magnet link'))
|
||||
.catch(err => {
|
||||
console.error('Failed to open magnet link:', err);
|
||||
// No good fallback for magnet links
|
||||
navigateToPlayer(stream);
|
||||
});
|
||||
} else {
|
||||
// For direct video URLs, use the S.Browser.ACTION_VIEW approach
|
||||
// This is a more reliable way to force Android to show all video apps
|
||||
|
||||
// Strip query parameters if they exist as they can cause issues with some apps
|
||||
let cleanUrl = stream.url;
|
||||
if (cleanUrl.includes('?')) {
|
||||
cleanUrl = cleanUrl.split('?')[0];
|
||||
}
|
||||
|
||||
// Create an Android intent URL that forces the chooser
|
||||
// Set component=null to ensure chooser is shown
|
||||
// Set action=android.intent.action.VIEW to open the content
|
||||
const intentUrl = `intent:${cleanUrl}#Intent;action=android.intent.action.VIEW;category=android.intent.category.DEFAULT;component=;type=video/*;launchFlags=0x10000000;end`;
|
||||
|
||||
console.log(`Using intent URL: ${intentUrl}`);
|
||||
|
||||
Linking.openURL(intentUrl)
|
||||
.then(() => console.log('Successfully opened with intent URL'))
|
||||
.catch(err => {
|
||||
console.error('Failed to open with intent URL:', err);
|
||||
|
||||
// First fallback: Try direct URL with regular Linking API
|
||||
console.log('Trying plain URL as fallback');
|
||||
Linking.openURL(stream.url)
|
||||
.then(() => console.log('Opened with direct URL'))
|
||||
.catch(directErr => {
|
||||
console.error('Failed to open direct URL:', directErr);
|
||||
|
||||
// Final fallback: Use built-in player
|
||||
console.log('All external player attempts failed, using built-in player');
|
||||
navigateToPlayer(stream);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with external player:', error);
|
||||
// Fallback to the built-in player
|
||||
navigateToPlayer(stream);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// For internal player or if other options failed, use the built-in player
|
||||
navigateToPlayer(stream);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Stream error:', error);
|
||||
Alert.alert(
|
||||
'Playback Error',
|
||||
error instanceof Error ? error.message : 'An error occurred while playing the video'
|
||||
);
|
||||
console.error('Error in handleStreamPress:', error);
|
||||
// Final fallback: Use built-in player
|
||||
navigateToPlayer(stream);
|
||||
}
|
||||
}, [metadata, type, currentEpisode, navigation, settings.useExternalPlayer]);
|
||||
}, [settings.preferredPlayer, settings.useExternalPlayer, navigateToPlayer]);
|
||||
|
||||
const filterItems = useMemo(() => {
|
||||
const installedAddons = stremioService.getInstalledAddons();
|
||||
|
|
|
|||
485
src/screens/TraktSettingsScreen.tsx
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Image,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
Linking
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { makeRedirectUri } from 'expo-auth-session';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { traktService, TraktUser } from '../services/traktService';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { logger } from '../utils/logger';
|
||||
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
// For use with deep linking
|
||||
const redirectUri = makeRedirectUri({
|
||||
scheme: 'stremioexpo',
|
||||
path: 'auth/trakt',
|
||||
});
|
||||
|
||||
const TraktSettingsScreen: React.FC = () => {
|
||||
const { settings } = useSettings();
|
||||
const isDarkMode = settings.enableDarkMode;
|
||||
const navigation = useNavigation();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
||||
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const authenticated = await traktService.isAuthenticated();
|
||||
setIsAuthenticated(authenticated);
|
||||
|
||||
if (authenticated) {
|
||||
const profile = await traktService.getUserProfile();
|
||||
setUserProfile(profile);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Error checking auth status:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
// Handle deep linking when returning from Trakt authorization
|
||||
useEffect(() => {
|
||||
const handleRedirect = async (event: { url: string }) => {
|
||||
const { url } = event;
|
||||
if (url.includes('auth/trakt')) {
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
const code = url.split('code=')[1].split('&')[0];
|
||||
const success = await traktService.exchangeCodeForToken(code);
|
||||
if (success) {
|
||||
checkAuthStatus();
|
||||
} else {
|
||||
Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Authentication error:', error);
|
||||
Alert.alert('Authentication Error', 'An error occurred during authentication.');
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener for deep linking
|
||||
const subscription = Linking.addEventListener('url', handleRedirect);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
const handleSignIn = async () => {
|
||||
try {
|
||||
const authUrl = traktService.getAuthUrl();
|
||||
await WebBrowser.openAuthSessionAsync(authUrl, redirectUri);
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Error opening auth session:', error);
|
||||
Alert.alert('Authentication Error', 'Could not open Trakt authentication page.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
Alert.alert(
|
||||
'Sign Out',
|
||||
'Are you sure you want to sign out of your Trakt account?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Sign Out',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await traktService.logout();
|
||||
setIsAuthenticated(false);
|
||||
setUserProfile(null);
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Error signing out:', error);
|
||||
Alert.alert('Error', 'Failed to sign out of Trakt.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
||||
]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={[
|
||||
styles.headerTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
Trakt Settings
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
]}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
) : isAuthenticated && userProfile ? (
|
||||
<View style={styles.profileContainer}>
|
||||
<View style={styles.profileHeader}>
|
||||
{userProfile.avatar ? (
|
||||
<Image
|
||||
source={{ uri: userProfile.avatar }}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.avatarPlaceholder, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.avatarText}>
|
||||
{userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.profileInfo}>
|
||||
<Text style={[
|
||||
styles.profileName,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
{userProfile.name || userProfile.username}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.profileUsername,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
@{userProfile.username}
|
||||
</Text>
|
||||
{userProfile.vip && (
|
||||
<View style={styles.vipBadge}>
|
||||
<Text style={styles.vipText}>VIP</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.statsContainer}>
|
||||
<Text style={[
|
||||
styles.joinedDate,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
styles.signOutButton,
|
||||
{ backgroundColor: isDarkMode ? 'rgba(255,59,48,0.1)' : 'rgba(255,59,48,0.08)' }
|
||||
]}
|
||||
onPress={handleSignOut}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: '#FF3B30' }]}>
|
||||
Sign Out
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.signInContainer}>
|
||||
<TraktIcon
|
||||
width={120}
|
||||
height={120}
|
||||
style={styles.traktLogo}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.signInTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
Connect with Trakt
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.signInDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
Sync your watch history, watchlist, and collection with Trakt.tv
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{ backgroundColor: isDarkMode ? colors.primary : colors.primary }
|
||||
]}
|
||||
onPress={handleSignIn}
|
||||
disabled={isAuthenticating}
|
||||
>
|
||||
{isAuthenticating ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>
|
||||
Sign In with Trakt
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isAuthenticated && (
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
]}>
|
||||
<View style={styles.settingsSection}>
|
||||
<Text style={[
|
||||
styles.sectionTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
Sync Settings
|
||||
</Text>
|
||||
<View style={styles.settingItem}>
|
||||
<Text style={[
|
||||
styles.settingLabel,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
Auto-sync playback progress
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
Coming soon
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.settingItem}>
|
||||
<Text style={[
|
||||
styles.settingLabel,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
]}>
|
||||
Import watched history
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
Coming soon
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{ backgroundColor: isDarkMode ? 'rgba(120,120,128,0.2)' : 'rgba(120,120,128,0.1)' }
|
||||
]}
|
||||
disabled={true}
|
||||
>
|
||||
<Text style={[
|
||||
styles.buttonText,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
]}>
|
||||
Sync Now (Coming Soon)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
|
||||
},
|
||||
backButton: {
|
||||
padding: 4,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '600',
|
||||
marginLeft: 16,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
loadingContainer: {
|
||||
padding: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
signInContainer: {
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
traktLogo: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
marginBottom: 20,
|
||||
},
|
||||
signInTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
signInDescription: {
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
height: 44,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
signOutButton: {
|
||||
marginTop: 20,
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: 'white',
|
||||
},
|
||||
profileContainer: {
|
||||
padding: 20,
|
||||
},
|
||||
profileHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
},
|
||||
avatarPlaceholder: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: 26,
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
},
|
||||
profileInfo: {
|
||||
marginLeft: 16,
|
||||
flex: 1,
|
||||
},
|
||||
profileName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
profileUsername: {
|
||||
fontSize: 14,
|
||||
},
|
||||
vipBadge: {
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: '#FFD700',
|
||||
borderRadius: 4,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
vipText: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
},
|
||||
statsContainer: {
|
||||
marginTop: 16,
|
||||
paddingTop: 16,
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: 'rgba(150,150,150,0.2)',
|
||||
},
|
||||
joinedDate: {
|
||||
fontSize: 14,
|
||||
},
|
||||
settingsSection: {
|
||||
padding: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 16,
|
||||
},
|
||||
settingItem: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
settingLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
marginBottom: 4,
|
||||
},
|
||||
settingDescription: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default TraktSettingsScreen;
|
||||
|
|
@ -3,6 +3,16 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||
import axios from 'axios';
|
||||
import { TMDBService } from './tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getCatalogDisplayName } from '../utils/catalogNameUtils';
|
||||
|
||||
// Add a constant for storing the data source preference
|
||||
const DATA_SOURCE_KEY = 'discover_data_source';
|
||||
|
||||
// Define data source types
|
||||
export enum DataSource {
|
||||
STREMIO_ADDONS = 'stremio_addons',
|
||||
TMDB = 'tmdb',
|
||||
}
|
||||
|
||||
export interface StreamingAddon {
|
||||
id: string;
|
||||
|
|
@ -55,6 +65,8 @@ export interface CatalogContent {
|
|||
items: StreamingContent[];
|
||||
}
|
||||
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
|
||||
class CatalogService {
|
||||
private static instance: CatalogService;
|
||||
private readonly LIBRARY_KEY = 'stremio-library';
|
||||
|
|
@ -137,43 +149,38 @@ class CatalogService {
|
|||
const addons = await this.getAllAddons();
|
||||
const catalogs: CatalogContent[] = [];
|
||||
|
||||
// Get saved catalog settings
|
||||
const savedSettings = await AsyncStorage.getItem('catalog_settings');
|
||||
const catalogSettings: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {};
|
||||
// Load enabled/disabled settings
|
||||
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||
|
||||
// Get featured catalogs
|
||||
// Process addons in order (they're already returned in order from getAllAddons)
|
||||
for (const addon of addons) {
|
||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
||||
// For each catalog, check if it's enabled in settings
|
||||
if (addon.catalogs) {
|
||||
for (const catalog of addon.catalogs) {
|
||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||
// If setting doesn't exist, default to true for backward compatibility
|
||||
const isEnabled = catalogSettings[settingKey] ?? true;
|
||||
|
||||
if (isEnabled) {
|
||||
try {
|
||||
// Get the items for this catalog
|
||||
const addonManifest = await stremioService.getInstalledAddonsAsync();
|
||||
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||
if (!manifest) continue;
|
||||
|
||||
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
||||
if (metas && metas.length > 0) {
|
||||
// Convert Meta to StreamingContent
|
||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||
|
||||
// Format the catalog name
|
||||
let displayName = catalog.name;
|
||||
// Get potentially custom display name
|
||||
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name);
|
||||
|
||||
// Remove duplicate words and clean up the name (case-insensitive)
|
||||
const words = displayName.split(' ');
|
||||
const uniqueWords = [];
|
||||
const seenWords = new Set();
|
||||
|
||||
for (const word of words) {
|
||||
const lowerWord = word.toLowerCase();
|
||||
if (!seenWords.has(lowerWord)) {
|
||||
uniqueWords.push(word); // Keep original case
|
||||
uniqueWords.push(word);
|
||||
seenWords.add(lowerWord);
|
||||
}
|
||||
}
|
||||
|
|
@ -194,7 +201,7 @@ class CatalogService {
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
||||
logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -205,10 +212,18 @@ class CatalogService {
|
|||
}
|
||||
|
||||
async getCatalogByType(type: string, genreFilter?: string): Promise<CatalogContent[]> {
|
||||
// Get the data source preference (default to Stremio addons)
|
||||
const dataSourcePreference = await this.getDataSourcePreference();
|
||||
|
||||
// If TMDB is selected as the data source, use TMDB API
|
||||
if (dataSourcePreference === DataSource.TMDB) {
|
||||
return this.getCatalogByTypeFromTMDB(type, genreFilter);
|
||||
}
|
||||
|
||||
// Otherwise use the original Stremio addons method
|
||||
const addons = await this.getAllAddons();
|
||||
const catalogs: CatalogContent[] = [];
|
||||
|
||||
// Filter addons with catalogs of the specified type
|
||||
const typeAddons = addons.filter(addon =>
|
||||
addon.catalogs && addon.catalogs.some(catalog => catalog.type === type)
|
||||
);
|
||||
|
|
@ -222,18 +237,20 @@ class CatalogService {
|
|||
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||
if (!manifest) continue;
|
||||
|
||||
// Apply genre filter if provided
|
||||
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
|
||||
const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters);
|
||||
|
||||
if (metas && metas.length > 0) {
|
||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||
|
||||
// Get potentially custom display name
|
||||
const displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name);
|
||||
|
||||
catalogs.push({
|
||||
addon: addon.id,
|
||||
type,
|
||||
id: catalog.id,
|
||||
name: catalog.name,
|
||||
name: displayName,
|
||||
genre: genreFilter,
|
||||
items
|
||||
});
|
||||
|
|
@ -247,6 +264,148 @@ class CatalogService {
|
|||
return catalogs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get catalog content from TMDB by type and genre
|
||||
*/
|
||||
private async getCatalogByTypeFromTMDB(type: string, genreFilter?: string): Promise<CatalogContent[]> {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const catalogs: CatalogContent[] = [];
|
||||
|
||||
try {
|
||||
// Map Stremio content type to TMDB content type
|
||||
const tmdbType = type === 'movie' ? 'movie' : 'tv';
|
||||
|
||||
// If no genre filter or All is selected, get multiple catalogs
|
||||
if (!genreFilter || genreFilter === 'All') {
|
||||
// Get trending
|
||||
const trendingItems = await tmdbService.getTrending(tmdbType, 'week');
|
||||
const trendingItemsPromises = trendingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
|
||||
const trendingStreamingItems = await Promise.all(trendingItemsPromises);
|
||||
|
||||
catalogs.push({
|
||||
addon: 'tmdb',
|
||||
type,
|
||||
id: 'trending',
|
||||
name: `Trending ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
|
||||
items: trendingStreamingItems
|
||||
});
|
||||
|
||||
// Get popular
|
||||
const popularItems = await tmdbService.getPopular(tmdbType, 1);
|
||||
const popularItemsPromises = popularItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
|
||||
const popularStreamingItems = await Promise.all(popularItemsPromises);
|
||||
|
||||
catalogs.push({
|
||||
addon: 'tmdb',
|
||||
type,
|
||||
id: 'popular',
|
||||
name: `Popular ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
|
||||
items: popularStreamingItems
|
||||
});
|
||||
|
||||
// Get upcoming/on air
|
||||
const upcomingItems = await tmdbService.getUpcoming(tmdbType, 1);
|
||||
const upcomingItemsPromises = upcomingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
|
||||
const upcomingStreamingItems = await Promise.all(upcomingItemsPromises);
|
||||
|
||||
catalogs.push({
|
||||
addon: 'tmdb',
|
||||
type,
|
||||
id: 'upcoming',
|
||||
name: type === 'movie' ? 'Upcoming Movies' : 'On Air TV Shows',
|
||||
items: upcomingStreamingItems
|
||||
});
|
||||
} else {
|
||||
// Get content by genre
|
||||
const genreItems = await tmdbService.discoverByGenre(tmdbType, genreFilter);
|
||||
const streamingItemsPromises = genreItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
|
||||
const streamingItems = await Promise.all(streamingItemsPromises);
|
||||
|
||||
catalogs.push({
|
||||
addon: 'tmdb',
|
||||
type,
|
||||
id: 'discover',
|
||||
name: `${genreFilter} ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
|
||||
genre: genreFilter,
|
||||
items: streamingItems
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get catalog from TMDB for type ${type}, genre ${genreFilter}:`, error);
|
||||
}
|
||||
|
||||
return catalogs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TMDB trending/discover result to StreamingContent format
|
||||
*/
|
||||
private async convertTMDBToStreamingContent(item: any, type: 'movie' | 'tv'): Promise<StreamingContent> {
|
||||
const id = item.external_ids?.imdb_id || `tmdb:${item.id}`;
|
||||
const name = type === 'movie' ? item.title : item.name;
|
||||
const posterPath = item.poster_path;
|
||||
|
||||
// Get genres from genre_ids
|
||||
let genres: string[] = [];
|
||||
if (item.genre_ids && item.genre_ids.length > 0) {
|
||||
try {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const genreLists = type === 'movie'
|
||||
? await tmdbService.getMovieGenres()
|
||||
: await tmdbService.getTvGenres();
|
||||
|
||||
const genreIds: number[] = item.genre_ids;
|
||||
genres = genreIds
|
||||
.map(genreId => {
|
||||
const genre = genreLists.find(g => g.id === genreId);
|
||||
return genre ? genre.name : null;
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
} catch (error) {
|
||||
logger.error('Failed to get genres for TMDB content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type: type === 'movie' ? 'movie' : 'series',
|
||||
name: name || 'Unknown',
|
||||
poster: posterPath ? `https://image.tmdb.org/t/p/w500${posterPath}` : 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
|
||||
posterShape: 'poster',
|
||||
banner: item.backdrop_path ? `https://image.tmdb.org/t/p/original${item.backdrop_path}` : undefined,
|
||||
year: type === 'movie'
|
||||
? (item.release_date ? new Date(item.release_date).getFullYear() : undefined)
|
||||
: (item.first_air_date ? new Date(item.first_air_date).getFullYear() : undefined),
|
||||
description: item.overview,
|
||||
genres,
|
||||
inLibrary: this.library[`${type === 'movie' ? 'movie' : 'series'}:${id}`] !== undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current data source preference
|
||||
*/
|
||||
async getDataSourcePreference(): Promise<DataSource> {
|
||||
try {
|
||||
const dataSource = await AsyncStorage.getItem(DATA_SOURCE_KEY);
|
||||
return dataSource as DataSource || DataSource.STREMIO_ADDONS;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get data source preference:', error);
|
||||
return DataSource.STREMIO_ADDONS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data source preference
|
||||
*/
|
||||
async setDataSourcePreference(dataSource: DataSource): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(DATA_SOURCE_KEY, dataSource);
|
||||
} catch (error) {
|
||||
logger.error('Failed to set data source preference:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getContentDetails(type: string, id: string): Promise<StreamingContent | null> {
|
||||
try {
|
||||
// Try up to 3 times with increasing delays
|
||||
|
|
|
|||