diff --git a/App.tsx b/App.tsx
index 24c504d..c4be52e 100644
--- a/App.tsx
+++ b/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 {
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f229e83
--- /dev/null
+++ b/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/README.md b/README.md
index da5e86d..370cdf3 100644
--- a/README.md
+++ b/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
\ No newline at end of file
+This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
\ No newline at end of file
diff --git a/android/.gitignore b/android/.gitignore
deleted file mode 100644
index 8a6be07..0000000
--- a/android/.gitignore
+++ /dev/null
@@ -1,16 +0,0 @@
-# OSX
-#
-.DS_Store
-
-# Android/IntelliJ
-#
-build/
-.idea
-.gradle
-local.properties
-*.iml
-*.hprof
-.cxx/
-
-# Bundle artifacts
-*.jsbundle
diff --git a/android/app/build.gradle b/android/app/build.gradle
deleted file mode 100644
index dc4e424..0000000
--- a/android/app/build.gradle
+++ /dev/null
@@ -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.. 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
- }
-}
diff --git a/android/app/debug.keystore b/android/app/debug.keystore
deleted file mode 100644
index 364e105..0000000
Binary files a/android/app/debug.keystore and /dev/null differ
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
deleted file mode 100644
index 551eb41..0000000
--- a/android/app/proguard-rules.pro
+++ /dev/null
@@ -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:
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index 3ec2507..0000000
--- a/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 26c117a..0000000
--- a/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/java/com/nuvio/app/MainActivity.kt b/android/app/src/main/java/com/nuvio/app/MainActivity.kt
deleted file mode 100644
index bdd6bfe..0000000
--- a/android/app/src/main/java/com/nuvio/app/MainActivity.kt
+++ /dev/null
@@ -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 onBackPressed
- */
- 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()
- }
-}
diff --git a/android/app/src/main/java/com/nuvio/app/MainApplication.kt b/android/app/src/main/java/com/nuvio/app/MainApplication.kt
deleted file mode 100644
index 31160e6..0000000
--- a/android/app/src/main/java/com/nuvio/app/MainApplication.kt
+++ /dev/null
@@ -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 {
- 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)
- }
-}
diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
deleted file mode 100644
index 31df827..0000000
Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
deleted file mode 100644
index ef243aa..0000000
Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
deleted file mode 100644
index e9d5474..0000000
Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
deleted file mode 100644
index d61da15..0000000
Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
deleted file mode 100644
index 4aeed11..0000000
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 883b2a0..0000000
--- a/android/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- -
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml
deleted file mode 100644
index 5c25e72..0000000
--- a/android/app/src/main/res/drawable/rn_edit_text_material.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 3941bea..0000000
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 3941bea..0000000
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index 7fae0cc..0000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
deleted file mode 100644
index ac03dbf..0000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index afa0a4e..0000000
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 78aaf45..0000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
deleted file mode 100644
index e1173a9..0000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index c4f6e10..0000000
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 7a0f085..0000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index ff086fd..0000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 6c2d40b..0000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 730e3fa..0000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index f7f1d06..0000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 3452615..0000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index b11a322..0000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index 49a464e..0000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index b51fd15..0000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml
deleted file mode 100644
index 3c05de5..0000000
--- a/android/app/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
deleted file mode 100644
index f387b90..0000000
--- a/android/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
- #ffffff
- #ffffff
- #023c69
- #ffffff
-
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
deleted file mode 100644
index 9a6c16e..0000000
--- a/android/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
- Nuvio
- contain
- false
- light
-
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index da52521..0000000
--- a/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
deleted file mode 100644
index abbcb8e..0000000
--- a/android/build.gradle
+++ /dev/null
@@ -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' }
- }
-}
diff --git a/android/gradle.properties b/android/gradle.properties
deleted file mode 100644
index 7531e9e..0000000
--- a/android/gradle.properties
+++ /dev/null
@@ -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 -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
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index a4b76b9..0000000
Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 79eb9d0..0000000
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -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
diff --git a/android/gradlew b/android/gradlew
deleted file mode 100644
index f5feea6..0000000
--- a/android/gradlew
+++ /dev/null
@@ -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" "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
deleted file mode 100644
index 9d21a21..0000000
--- a/android/gradlew.bat
+++ /dev/null
@@ -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
diff --git a/android/settings.gradle b/android/settings.gradle
deleted file mode 100644
index a39f8ed..0000000
--- a/android/settings.gradle
+++ /dev/null
@@ -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())
diff --git a/assets/trakt-logo.png b/assets/trakt-logo.png
new file mode 100644
index 0000000..3b237e4
--- /dev/null
+++ b/assets/trakt-logo.png
@@ -0,0 +1,2 @@
+// This is a placeholder for a binary PNG file
+// Replace this file with an actual Trakt logo image
\ No newline at end of file
diff --git a/ios/.gitignore b/ios/.gitignore
deleted file mode 100644
index 8beb344..0000000
--- a/ios/.gitignore
+++ /dev/null
@@ -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/
diff --git a/ios/.xcode.env b/ios/.xcode.env
deleted file mode 100644
index 3d5782c..0000000
--- a/ios/.xcode.env
+++ /dev/null
@@ -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)
diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj
deleted file mode 100644
index 7f56bb4..0000000
--- a/ios/Nuvio.xcodeproj/project.pbxproj
+++ /dev/null
@@ -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 = ""; };
- 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Nuvio/AppDelegate.mm; sourceTree = ""; };
- 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = ""; };
- 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = ""; };
- 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Nuvio/main.m; sourceTree = ""; };
- 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 = ""; };
- 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 = ""; };
- AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = ""; };
- BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; };
- 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 = ""; };
- DC1EB3A22DCB47A0A12A0CAF /* noop-file.swift */ = {isa = PBXFileReference; name = "noop-file.swift"; path = "Nuvio/noop-file.swift"; sourceTree = ""; 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 = ""; 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 = "";
- };
- 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
- 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Nuvio.a */,
- );
- name = Frameworks;
- sourceTree = "";
- };
- 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
- isa = PBXGroup;
- children = (
- );
- name = Libraries;
- sourceTree = "";
- };
- 83CBB9F61A601CBA00E9B192 = {
- isa = PBXGroup;
- children = (
- 13B07FAE1A68108700A75B9A /* Nuvio */,
- 832341AE1AAA6A7D00B99B32 /* Libraries */,
- 83CBBA001A601CBA00E9B192 /* Products */,
- 2D16E6871FA4F8E400B85C8A /* Frameworks */,
- D65327D7A22EEC0BE12398D9 /* Pods */,
- D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
- );
- indentWidth = 2;
- sourceTree = "";
- tabWidth = 2;
- usesTabs = 0;
- };
- 83CBBA001A601CBA00E9B192 /* Products */ = {
- isa = PBXGroup;
- children = (
- 13B07F961A680F5B00A75B9A /* Nuvio.app */,
- );
- name = Products;
- sourceTree = "";
- };
- 92DBD88DE9BF7D494EA9DA96 /* Nuvio */ = {
- isa = PBXGroup;
- children = (
- FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */,
- );
- name = Nuvio;
- sourceTree = "";
- };
- BB2F792B24A3F905000567C9 /* Supporting */ = {
- isa = PBXGroup;
- children = (
- BB2F792C24A3F905000567C9 /* Expo.plist */,
- );
- name = Supporting;
- path = Nuvio/Supporting;
- sourceTree = "";
- };
- D65327D7A22EEC0BE12398D9 /* Pods */ = {
- isa = PBXGroup;
- children = (
- 6C2E3173556A471DD304B334 /* Pods-Nuvio.debug.xcconfig */,
- 7A4D352CD337FB3A3BF06240 /* Pods-Nuvio.release.xcconfig */,
- );
- path = Pods;
- sourceTree = "";
- };
- D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
- isa = PBXGroup;
- children = (
- 92DBD88DE9BF7D494EA9DA96 /* Nuvio */,
- );
- name = ExpoModulesProviders;
- sourceTree = "";
- };
-/* 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 */;
-}
diff --git a/ios/Nuvio.xcodeproj/xcshareddata/xcschemes/Nuvio.xcscheme b/ios/Nuvio.xcodeproj/xcshareddata/xcschemes/Nuvio.xcscheme
deleted file mode 100644
index d56adf8..0000000
--- a/ios/Nuvio.xcodeproj/xcshareddata/xcschemes/Nuvio.xcscheme
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ios/Nuvio/AppDelegate.h b/ios/Nuvio/AppDelegate.h
deleted file mode 100644
index 1658a43..0000000
--- a/ios/Nuvio/AppDelegate.h
+++ /dev/null
@@ -1,7 +0,0 @@
-#import
-#import
-#import
-
-@interface AppDelegate : EXAppDelegateWrapper
-
-@end
diff --git a/ios/Nuvio/AppDelegate.mm b/ios/Nuvio/AppDelegate.mm
deleted file mode 100644
index b27f832..0000000
--- a/ios/Nuvio/AppDelegate.mm
+++ /dev/null
@@ -1,62 +0,0 @@
-#import "AppDelegate.h"
-
-#import
-#import
-
-@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 *)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> * _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
diff --git a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png
deleted file mode 100644
index d63e7ac..0000000
Binary files a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and /dev/null differ
diff --git a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 90d8d4c..0000000
--- a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "images": [
- {
- "filename": "App-Icon-1024x1024@1x.png",
- "idiom": "universal",
- "platform": "ios",
- "size": "1024x1024"
- }
- ],
- "info": {
- "version": 1,
- "author": "expo"
- }
-}
\ No newline at end of file
diff --git a/ios/Nuvio/Images.xcassets/Contents.json b/ios/Nuvio/Images.xcassets/Contents.json
deleted file mode 100644
index ed285c2..0000000
--- a/ios/Nuvio/Images.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "expo"
- }
-}
diff --git a/ios/Nuvio/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/Nuvio/Images.xcassets/SplashScreenBackground.colorset/Contents.json
deleted file mode 100644
index 15f02ab..0000000
--- a/ios/Nuvio/Images.xcassets/SplashScreenBackground.colorset/Contents.json
+++ /dev/null
@@ -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"
- }
-}
\ No newline at end of file
diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/Contents.json
deleted file mode 100644
index f65c008..0000000
--- a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/Contents.json
+++ /dev/null
@@ -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"
- }
-}
\ No newline at end of file
diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image.png b/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image.png
deleted file mode 100644
index b9ff0fc..0000000
Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image.png and /dev/null differ
diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@2x.png
deleted file mode 100644
index b9ff0fc..0000000
Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@2x.png and /dev/null differ
diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@3x.png
deleted file mode 100644
index b9ff0fc..0000000
Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLogo.imageset/image@3x.png and /dev/null differ
diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist
deleted file mode 100644
index 30cbd48..0000000
--- a/ios/Nuvio/Info.plist
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Nuvio
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.0.0
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- com.nuvio.app
-
-
-
- CFBundleVersion
- 1
- LSMinimumSystemVersion
- 12.0
- LSRequiresIPhoneOS
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- UILaunchStoryboardName
- SplashScreen
- UIRequiredDeviceCapabilities
-
- arm64
-
- UIRequiresFullScreen
-
- UIStatusBarStyle
- UIStatusBarStyleDefault
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIUserInterfaceStyle
- Light
- UIViewControllerBasedStatusBarAppearance
-
-
-
\ No newline at end of file
diff --git a/ios/Nuvio/Nuvio-Bridging-Header.h b/ios/Nuvio/Nuvio-Bridging-Header.h
deleted file mode 100644
index e11d920..0000000
--- a/ios/Nuvio/Nuvio-Bridging-Header.h
+++ /dev/null
@@ -1,3 +0,0 @@
-//
-// Use this file to import your target's public headers that you would like to expose to Swift.
-//
diff --git a/ios/Nuvio/Nuvio.entitlements b/ios/Nuvio/Nuvio.entitlements
deleted file mode 100644
index 018a6e2..0000000
--- a/ios/Nuvio/Nuvio.entitlements
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- aps-environment
- development
-
-
\ No newline at end of file
diff --git a/ios/Nuvio/SplashScreen.storyboard b/ios/Nuvio/SplashScreen.storyboard
deleted file mode 100644
index 8a6fcd4..0000000
--- a/ios/Nuvio/SplashScreen.storyboard
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ios/Nuvio/Supporting/Expo.plist b/ios/Nuvio/Supporting/Expo.plist
deleted file mode 100644
index 750be02..0000000
--- a/ios/Nuvio/Supporting/Expo.plist
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- EXUpdatesCheckOnLaunch
- ALWAYS
- EXUpdatesEnabled
-
- EXUpdatesLaunchWaitMs
- 0
-
-
\ No newline at end of file
diff --git a/ios/Nuvio/main.m b/ios/Nuvio/main.m
deleted file mode 100644
index 25181b6..0000000
--- a/ios/Nuvio/main.m
+++ /dev/null
@@ -1,10 +0,0 @@
-#import
-
-#import "AppDelegate.h"
-
-int main(int argc, char * argv[]) {
- @autoreleasepool {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
-}
-
diff --git a/ios/Nuvio/noop-file.swift b/ios/Nuvio/noop-file.swift
deleted file mode 100644
index b2ffafb..0000000
--- a/ios/Nuvio/noop-file.swift
+++ /dev/null
@@ -1,4 +0,0 @@
-//
-// @generated
-// A blank Swift file must be created for native modules with Swift files to work correctly.
-//
diff --git a/ios/Podfile b/ios/Podfile
deleted file mode 100644
index a7242ef..0000000
--- a/ios/Podfile
+++ /dev/null
@@ -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
diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json
deleted file mode 100644
index 417e2e5..0000000
--- a/ios/Podfile.properties.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "expo.jsEngine": "hermes",
- "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
- "newArchEnabled": "true"
-}
diff --git a/package-lock.json b/package-lock.json
index f019a51..5ef1cbb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 61ad751..99902dd 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/src/assets/discover.jpg b/src/assets/discover.jpg
new file mode 100644
index 0000000..9e2a368
Binary files /dev/null and b/src/assets/discover.jpg differ
diff --git a/src/assets/home.jpg b/src/assets/home.jpg
new file mode 100644
index 0000000..5c11c74
Binary files /dev/null and b/src/assets/home.jpg differ
diff --git a/src/assets/metadascreen.jpg b/src/assets/metadascreen.jpg
new file mode 100644
index 0000000..c8a2ff5
Binary files /dev/null and b/src/assets/metadascreen.jpg differ
diff --git a/src/assets/ratingscreen.jpg b/src/assets/ratingscreen.jpg
new file mode 100644
index 0000000..b2d723c
Binary files /dev/null and b/src/assets/ratingscreen.jpg differ
diff --git a/src/assets/search.jpg b/src/assets/search.jpg
new file mode 100644
index 0000000..2c176f9
Binary files /dev/null and b/src/assets/search.jpg differ
diff --git a/src/assets/seasonandepisode.jpg b/src/assets/seasonandepisode.jpg
new file mode 100644
index 0000000..336dab5
Binary files /dev/null and b/src/assets/seasonandepisode.jpg differ
diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx
index 1382a19..c41cb89 100644
--- a/src/components/metadata/SeriesContent.tsx
+++ b/src/components/metadata/SeriesContent.tsx
@@ -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 = ({
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(null);
const loadEpisodesProgress = async () => {
if (!metadata?.id) return;
@@ -70,6 +73,25 @@ export const SeriesContent: React.FC = ({
}, [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 (
@@ -99,6 +121,7 @@ export const SeriesContent: React.FC = ({
Seasons
= ({ 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]);
}, []);
diff --git a/src/contexts/TraktContext.tsx b/src/contexts/TraktContext.tsx
new file mode 100644
index 0000000..b7949cb
--- /dev/null
+++ b/src/contexts/TraktContext.tsx
@@ -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;
+ loadWatchedItems: () => Promise;
+ isMovieWatched: (imdbId: string) => Promise;
+ isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise;
+ markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise;
+ markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise;
+}
+
+const TraktContext = createContext(undefined);
+
+export function TraktProvider({ children }: { children: ReactNode }) {
+ const traktIntegration = useTraktIntegration();
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTraktContext() {
+ const context = useContext(TraktContext);
+ if (context === undefined) {
+ throw new Error('useTraktContext must be used within a TraktProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/src/hooks/useCustomCatalogNames.ts b/src/hooks/useCustomCatalogNames.ts
new file mode 100644
index 0000000..adb1d27
--- /dev/null
+++ b/src/hooks/useCustomCatalogNames.ts
@@ -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 };
+}
\ No newline at end of file
diff --git a/src/hooks/useHomeCatalogs.ts b/src/hooks/useHomeCatalogs.ts
index 60811a9..429fdbf 100644
--- a/src/hooks/useHomeCatalogs.ts
+++ b/src/hooks/useHomeCatalogs.ts
@@ -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([]);
@@ -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 () => {
diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts
index 2932339..cef08d8 100644
--- a/src/hooks/useMetadata.ts
+++ b/src/hooks/useMetadata.ts
@@ -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>;
+ 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([]);
const [loadingRecommendations, setLoadingRecommendations] = useState(false);
+ const [imdbId, setImdbId] = useState(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,
};
};
\ No newline at end of file
diff --git a/src/hooks/usePersistentSeasons.ts b/src/hooks/usePersistentSeasons.ts
new file mode 100644
index 0000000..bfb6bc7
--- /dev/null
+++ b/src/hooks/usePersistentSeasons.ts
@@ -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
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 5659586..426f44e 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -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 (
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);
}
diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts
new file mode 100644
index 0000000..d89ac19
--- /dev/null
+++ b/src/hooks/useTraktIntegration.ts
@@ -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(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [userProfile, setUserProfile] = useState(null);
+ const [watchedMovies, setWatchedMovies] = useState([]);
+ const [watchedShows, setWatchedShows] = useState([]);
+
+ // 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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
+ };
+}
\ No newline at end of file
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index 551410e..b1cda31 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -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;
@@ -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 = () => {
},
}}
/>
+
+
>
diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx
index e6c2882..4af15cd 100644
--- a/src/screens/AddonsScreen.tsx
+++ b/src/screens/AddonsScreen.tsx
@@ -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([]);
+ const [communityLoading, setCommunityLoading] = useState(true);
+ const [communityError, setCommunityError] = useState(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('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 (
-
+
+ {reorderMode && (
+
+ moveAddonUp(item)}
+ disabled={isFirstItem}
+ >
+
+
+ moveAddonDown(item)}
+ disabled={isLastItem}
+ >
+
+
+
+ )}
+
{logo ? (
{
{categoryText}
- handleToggleAddon(item, !value)}
- trackColor={{ false: colors.elevation1, true: colors.primary }}
- thumbColor={colors.white}
- ios_backgroundColor={colors.elevation1}
- />
+
+ {!reorderMode ? (
+ <>
+ {isConfigurable && (
+ handleConfigureAddon(item, item.transport)}
+ >
+
+
+ )}
+ handleRemoveAddon(item)}
+ >
+
+
+ >
+ ) : (
+
+ #{index + 1}
+
+ )}
+
@@ -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 (
+
+ {logo ? (
+
+ ) : (
+
+
+
+ )}
+
+ {manifest.name}
+ {description}
+
+ v{manifest.version || 'N/A'}
+ •
+ {categoryText}
+
+
+
+ {isConfigurable && (
+ handleConfigureAddon(manifest, transportUrl)}
+ >
+
+
+ )}
+ handleAddAddon(transportUrl)}
+ disabled={installing}
+ >
+ {installing ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ };
+
const StatsCard = ({ value, label }: { value: number; label: string }) => (
{value}
@@ -236,9 +514,48 @@ const AddonsScreen = () => {
Settings
+
+
+ {/* Reorder Mode Toggle Button */}
+
+
+
+
+ {/* Refresh Button */}
+
+
+
+
- Addons
+
+ Addons
+ {reorderMode && (Reorder Mode)}
+
+
+ {reorderMode && (
+
+
+
+ Addons at the top have higher priority when loading content
+
+
+ )}
{loading ? (
@@ -256,40 +573,44 @@ const AddonsScreen = () => {
-
+
- {/* Add Addon Section */}
-
- ADD NEW ADDON
-
-
-
-
- {installing ? 'Loading...' : 'Add Addon'}
-
-
+ {/* Hide Add Addon Section in reorder mode */}
+ {!reorderMode && (
+
+ ADD NEW ADDON
+
+
+ handleAddAddon()}
+ disabled={installing || !addonUrl}
+ >
+
+ {installing ? 'Loading...' : 'Add Addon'}
+
+
+
-
+ )}
{/* Installed Addons Section */}
- INSTALLED ADDONS
+
+ {reorderMode ? "DRAG ADDONS TO REORDER" : "INSTALLED ADDONS"}
+
{addons.length === 0 ? (
@@ -297,20 +618,103 @@ const AddonsScreen = () => {
No addons installed
) : (
- addons.map((addon, index) => {
- const isLast = index === addons.length - 1;
- return (
-
- {renderAddonItem({ item: addon })}
+ addons.map((addon, index) => (
+
+ {renderAddonItem({ item: addon, index })}
+
+ ))
+ )}
+
+
+
+ {/* Separator */}
+
+
+ {/* Community Addons Section */}
+
+ COMMUNITY ADDONS
+
+ {communityLoading ? (
+
+
+
+ ) : communityError ? (
+
+
+ {communityError}
+
+ ) : communityAddons.length === 0 ? (
+
+
+ No community addons available
+
+ ) : (
+ communityAddons.map((item, index) => (
+
+
+
+ {item.manifest.logo ? (
+
+ ) : (
+
+
+
+ )}
+
+ {item.manifest.name}
+
+ v{item.manifest.version || 'N/A'}
+ •
+
+ {item.manifest.types && item.manifest.types.length > 0
+ ? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
+ : 'General'}
+
+
+
+
+ {item.manifest.behaviorHints?.configurable && (
+ handleConfigureAddon(item.manifest, item.transportUrl)}
+ >
+
+
+ )}
+ handleAddAddon(item.transportUrl)}
+ disabled={installing}
+ >
+ {installing ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {item.manifest.description
+ ? (item.manifest.description.length > 100
+ ? item.manifest.description.substring(0, 100) + '...'
+ : item.manifest.description)
+ : 'No description provided.'}
+
- );
- })
+
+ ))
)}
@@ -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;
\ No newline at end of file
diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx
index cefcbfa..eafb36e 100644
--- a/src/screens/CatalogScreen.tsx
+++ b/src/screens/CatalogScreen.tsx
@@ -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;
@@ -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 = ({ route, navigation }) => {
- const { addonId, type, id, name, genreFilter } = route.params;
+ const { addonId, type, id, name: originalName, genreFilter } = route.params;
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState(null);
- // Force dark mode
+ const [dataSource, setDataSource] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ route, navigation }) => {
);
- if (loading && items.length === 0) {
+ const isScreenLoading = loading || isLoadingCustomNames;
+
+ if (isScreenLoading && items.length === 0) {
return (
@@ -259,7 +380,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
Back
- {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
+ {displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
{renderLoadingState()}
);
@@ -278,7 +399,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
Back
- {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
+ {displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
{renderErrorState()}
);
@@ -296,7 +417,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
Back
- {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
+ {displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
{items.length > 0 ? (
{
@@ -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(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();
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 = () => {
{group.expanded && group.catalogs.map((setting, index) => (
-
+ handleLongPress(setting)} // Added long press handler
+ style={({ pressed }) => [
+ styles.catalogItem,
+ pressed && styles.catalogItemPressed, // Optional pressed style
+ ]}
+ >
- {setting.name}
+ {setting.customName || setting.name} {/* Display custom or default name */}
{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"
/>
-
+
))}
))}
-
-
- ORGANIZATION
-
-
- Reorder Sections
-
-
-
- Customize Names
-
-
-
-
+
+ {/* Rename Modal */}
+ {
+ setIsRenameModalVisible(false);
+ setCatalogToRename(null);
+ }}
+ >
+ {Platform.OS === 'ios' ? (
+ setIsRenameModalVisible(false)}>
+
+ e.stopPropagation()}>
+ Rename Catalog
+
+
+
+
+
+
+ ) : (
+ setIsRenameModalVisible(false)}>
+ e.stopPropagation()}>
+ Rename Catalog
+
+
+ setIsRenameModalVisible(false)} color={colors.mediumGray} />
+
+
+
+
+ )}
+
+
);
};
@@ -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')
},
});
diff --git a/src/screens/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx
index f732dd9..9672ef2 100644
--- a/src/screens/DiscoverScreen.tsx
+++ b/src/screens/DiscoverScreen.tsx
@@ -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(() => , []);
diff --git a/src/screens/HeroCatalogsScreen.tsx b/src/screens/HeroCatalogsScreen.tsx
index 258ae94..2fa27d7 100644
--- a/src/screens/HeroCatalogsScreen.tsx
+++ b/src/screens/HeroCatalogsScreen.tsx
@@ -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([]);
const [selectedCatalogs, setSelectedCatalogs] = useState(settings.selectedHeroCatalogs || []);
+ const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
const handleBack = useCallback(() => {
navigation.goBack();
@@ -125,7 +127,7 @@ const HeroCatalogsScreen: React.FC = () => {
- {loading ? (
+ {loading || isLoadingCustomNames ? (
@@ -175,30 +177,35 @@ const HeroCatalogsScreen: React.FC = () => {
styles.catalogsContainer,
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
]}>
- {addonCatalogs.map(catalog => (
- toggleCatalog(catalog.id)}
- >
-
-
- {catalog.name}
-
-
- {catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
-
-
-
-
- ))}
+ {addonCatalogs.map(catalog => {
+ const [addonId, type, catalogId] = catalog.id.split(':');
+ const displayName = getCustomName(addonId, type, catalogId, catalog.name);
+
+ return (
+ toggleCatalog(catalog.id)}
+ >
+
+
+ {displayName}
+
+
+ {catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
+
+
+
+
+ );
+ })}
))}
diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx
index ccd025c..6632664 100644
--- a/src/screens/MetadataScreen.tsx
+++ b/src/screens/MetadataScreen.tsx
@@ -142,7 +142,7 @@ const ActionButtons = React.memo(({
}
}}
>
-
+
)}
@@ -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 = () => {
{/* Add RatingsSection right under the main metadata */}
- {id && (
+ {imdbId && (
)}
diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx
new file mode 100644
index 0000000..f1530dc
--- /dev/null
+++ b/src/screens/PlayerSettingsScreen.tsx
@@ -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 = ({
+ title,
+ description,
+ icon,
+ isDarkMode,
+ isSelected,
+ onPress,
+ isLast,
+}) => (
+
+
+
+
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {isSelected && (
+
+ )}
+
+
+);
+
+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 (
+
+
+
+
+
+
+
+
+ Video Player
+
+
+
+
+
+
+ PLAYER SELECTION
+
+
+ {playerOptions.map((option, index) => (
+ {
+ if (Platform.OS === 'ios') {
+ updateSetting('preferredPlayer', option.id as AppSettings['preferredPlayer'], false);
+ } else {
+ updateSetting('useExternalPlayer', option.id === 'external', false);
+ }
+ }}
+ isLast={index === playerOptions.length - 1}
+ />
+ ))}
+
+
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index 2d5e726..3b61c4a 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -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 = ({ children, isDarkMode }) => (
-
- {children}
+const SettingsCard: React.FC = ({ children, isDarkMode, title }) => (
+
+ {title && (
+
+ {title.toUpperCase()}
+
+ )}
+
+ {children}
+
);
@@ -51,6 +65,7 @@ interface SettingItemProps {
isLast?: boolean;
onPress?: () => void;
isDarkMode: boolean;
+ badge?: string | number;
}
const SettingItem: React.FC = ({
@@ -60,7 +75,8 @@ const SettingItem: React.FC = ({
renderControl,
isLast = false,
onPress,
- isDarkMode
+ isDarkMode,
+ badge
}) => {
return (
= ({
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
]}
>
-
-
+
+
-
+
{title}
@@ -86,6 +105,11 @@ const SettingItem: React.FC = ({
)}
+ {badge && (
+
+ {badge}
+
+ )}
{renderControl()}
@@ -94,28 +118,19 @@ const SettingItem: React.FC = ({
);
};
-const SectionHeader: React.FC<{ title: string; isDarkMode: boolean }> = ({ title, isDarkMode }) => (
-
-
- {title}
-
-
-);
-
const SettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
const systemColorScheme = useColorScheme();
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
const navigation = useNavigation>();
const { lastUpdate } = useCatalogContext();
+ const { isAuthenticated, userProfile } = useTraktContext();
// States for dynamic content
const [addonCount, setAddonCount] = useState(0);
const [catalogCount, setCatalogCount] = useState(0);
const [mdblistKeySet, setMdblistKeySet] = useState(false);
+ const [discoverDataSource, setDiscoverDataSource] = useState(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 = () => (
);
+ // Handle data source change
+ const handleDiscoverDataSourceChange = useCallback(async (value: string) => {
+ const dataSource = value as DataSource;
+ setDiscoverDataSource(dataSource);
+ await catalogService.setDataSourcePreference(dataSource);
+ }, []);
+
return (
{
Settings
+
+ Reset
+
-
-
+
Alert.alert('Trakt', 'Trakt integration coming soon')}
- />
- navigation.navigate('TraktSettings')}
isLast={true}
/>
-
-
+
+ navigation.navigate('Calendar')}
+ isDarkMode={isDarkMode}
+ />
+ navigation.navigate('NotificationSettings')}
+ isDarkMode={isDarkMode}
+ isLast={true}
+ />
+
+
+
navigation.navigate('Addons')}
+ badge={addonCount}
/>
navigation.navigate('CatalogSettings')}
+ badge={catalogCount}
/>
navigation.navigate('HomeScreenSettings')}
/>
-
{
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TMDBSettings')}
- />
-
-
-
-
+
- navigation.navigate('PlayerSettings')}
isLast={true}
/>
+
+
+ (
+
+ handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
+ >
+ Addons
+
+ handleDiscoverDataSourceChange(DataSource.TMDB)}
+ >
+ TMDB
+
+
+ )}
+ />
+
+
+
+
+ Version 1.0.0
+
+
);
@@ -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',
},
});
diff --git a/src/screens/ShowRatingsScreen.tsx b/src/screens/ShowRatingsScreen.tsx
index 011fae7..04e8e1b 100644
--- a/src/screens/ShowRatingsScreen.tsx
+++ b/src/screens/ShowRatingsScreen.tsx
@@ -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 (
-
+
+ {Platform.OS === 'ios' && (
+
+ )}
{
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,
},
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index b530639..faf8284 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -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();
diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx
new file mode 100644
index 0000000..cf89f56
--- /dev/null
+++ b/src/screens/TraktSettingsScreen.tsx
@@ -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(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 (
+
+
+
+ navigation.goBack()}
+ style={styles.backButton}
+ >
+
+
+
+ Trakt Settings
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : isAuthenticated && userProfile ? (
+
+
+ {userProfile.avatar ? (
+
+ ) : (
+
+
+ {userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
+
+
+ )}
+
+
+ {userProfile.name || userProfile.username}
+
+
+ @{userProfile.username}
+
+ {userProfile.vip && (
+
+ VIP
+
+ )}
+
+
+
+
+
+ Joined {new Date(userProfile.joined_at).toLocaleDateString()}
+
+
+
+
+
+ Sign Out
+
+
+
+ ) : (
+
+
+
+ Connect with Trakt
+
+
+ Sync your watch history, watchlist, and collection with Trakt.tv
+
+
+ {isAuthenticating ? (
+
+ ) : (
+
+ Sign In with Trakt
+
+ )}
+
+
+ )}
+
+
+ {isAuthenticated && (
+
+
+
+ Sync Settings
+
+
+
+ Auto-sync playback progress
+
+
+ Coming soon
+
+
+
+
+ Import watched history
+
+
+ Coming soon
+
+
+
+
+ Sync Now (Coming Soon)
+
+
+
+
+ )}
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts
index ed0b5b8..e76bef4 100644
--- a/src/services/catalogService.ts
+++ b/src/services/catalogService.ts
@@ -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 {
+ // 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
try {
// Try up to 3 times with increasing delays
diff --git a/src/services/mdblistService.ts b/src/services/mdblistService.ts
index cef6ae8..b5c8709 100644
--- a/src/services/mdblistService.ts
+++ b/src/services/mdblistService.ts
@@ -20,6 +20,9 @@ export class MDBListService {
private static instance: MDBListService;
private apiKey: string | null = null;
private enabled: boolean = true;
+ private apiKeyErrorCount: number = 0; // Add counter for API key errors
+ private lastApiKeyErrorTime: number = 0; // To track when last error occurred
+ private ratingsCache: Map = new Map(); // Cache for ratings - null values represent known "not found" results
private constructor() {
logger.log('[MDBListService] Service initialized');
@@ -36,16 +39,32 @@ export class MDBListService {
try {
// First check if MDBList is enabled
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
+ const wasEnabled = this.enabled;
this.enabled = enabledSetting === null || enabledSetting === 'true';
logger.log('[MDBListService] MDBList enabled:', this.enabled);
+ // Clear cache if enabled state changed
+ if (wasEnabled !== this.enabled) {
+ this.clearCache();
+ logger.log('[MDBListService] Cache cleared due to enabled state change');
+ }
+
if (!this.enabled) {
logger.log('[MDBListService] MDBList is disabled, skipping API key loading');
this.apiKey = null;
return;
}
- this.apiKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
+ const newApiKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
+ // Reset error counter when API key changes
+ if (newApiKey !== this.apiKey) {
+ this.apiKeyErrorCount = 0;
+ this.lastApiKeyErrorTime = 0;
+ // Clear the cache when API key changes
+ this.clearCache();
+ logger.log('[MDBListService] Cache cleared due to API key change');
+ }
+ this.apiKey = newApiKey;
logger.log('[MDBListService] Initialized with API key:', this.apiKey ? 'Present' : 'Not found');
} catch (error) {
logger.error('[MDBListService] Failed to load settings:', error);
@@ -57,6 +76,17 @@ export class MDBListService {
async getRatings(imdbId: string, mediaType: 'movie' | 'show'): Promise {
logger.log(`[MDBListService] Fetching ratings for ${mediaType} with IMDB ID:`, imdbId);
+ // Create cache key
+ const cacheKey = `${mediaType}:${imdbId}`;
+
+ // Check cache first - including null values which mean "no ratings available"
+ if (this.ratingsCache.has(cacheKey)) {
+ const cachedRatings = this.ratingsCache.get(cacheKey);
+ logger.log(`[MDBListService] Retrieved ${cachedRatings ? 'ratings' : 'negative result'} from cache for ${mediaType}:`, imdbId);
+ // TypeScript knows cachedRatings can't be undefined here since we checked with .has()
+ return cachedRatings as MDBListRatings | null;
+ }
+
// Check if MDBList is enabled before doing anything else
if (!this.enabled) {
// Try to refresh enabled status in case it was changed
@@ -139,7 +169,13 @@ export class MDBListService {
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error === "Invalid API key") {
- logger.error('[MDBListService] API Key rejected by server:', this.apiKey);
+ // Only log the error every 5 requests or if more than 10 minutes have passed
+ const now = Date.now();
+ this.apiKeyErrorCount++;
+ if (this.apiKeyErrorCount === 1 || this.apiKeyErrorCount % 5 === 0 || now - this.lastApiKeyErrorTime > 600000) {
+ logger.error('[MDBListService] API Key rejected by server:', this.apiKey);
+ this.lastApiKeyErrorTime = now;
+ }
} else {
logger.warn(`[MDBListService] 403 Forbidden, but not invalid key error:`, errorJson);
}
@@ -171,12 +207,37 @@ export class MDBListService {
const ratingCount = Object.keys(ratings).length;
logger.log(`[MDBListService] Fetched ${ratingCount} ratings successfully:`, ratings);
- return ratingCount > 0 ? ratings : null;
+
+ // Store in cache even if we got no ratings - this prevents repeated API calls for content with no ratings
+ const result = ratingCount > 0 ? ratings : null;
+ this.ratingsCache.set(cacheKey, result);
+ logger.log(`[MDBListService] Stored ${result ? 'ratings' : 'negative result'} in cache for ${mediaType}:`, imdbId);
+
+ return result;
} catch (error) {
logger.error('[MDBListService] Error fetching MDBList ratings:', error);
return null;
}
}
+
+ // Method to clear the cache
+ clearCache(): void {
+ this.ratingsCache.clear();
+ logger.log('[MDBListService] Cache cleared');
+ }
+
+ // Method to invalidate a specific cache entry
+ invalidateCache(imdbId: string, mediaType: 'movie' | 'show'): void {
+ const cacheKey = `${mediaType}:${imdbId}`;
+ const hadEntry = this.ratingsCache.delete(cacheKey);
+ logger.log(`[MDBListService] Cache entry ${hadEntry ? 'invalidated' : 'not found'} for ${mediaType}:`, imdbId);
+ }
+
+ // Method to check if a rating is in cache
+ isCached(imdbId: string, mediaType: 'movie' | 'show'): boolean {
+ const cacheKey = `${mediaType}:${imdbId}`;
+ return this.ratingsCache.has(cacheKey);
+ }
}
export const mdblistService = MDBListService.getInstance();
diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts
index 1be1d50..bad89ff 100644
--- a/src/services/stremioService.ts
+++ b/src/services/stremioService.ts
@@ -1,6 +1,15 @@
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { logger } from '../utils/logger';
+import EventEmitter from 'eventemitter3';
+
+// Create an event emitter for addon changes
+export const addonEmitter = new EventEmitter();
+export const ADDON_EVENTS = {
+ ORDER_CHANGED: 'order_changed',
+ ADDON_ADDED: 'addon_added',
+ ADDON_REMOVED: 'addon_removed'
+};
// Basic types for Stremio
export interface Meta {
@@ -137,7 +146,9 @@ export interface AddonCapabilities {
class StremioService {
private static instance: StremioService;
private installedAddons: Map = new Map();
+ private addonOrder: string[] = [];
private readonly STORAGE_KEY = 'stremio-addons';
+ private readonly ADDON_ORDER_KEY = 'stremio-addon-order';
private readonly DEFAULT_ADDONS = [
'https://v3-cinemeta.strem.io/manifest.json',
'https://opensubtitles-v3.strem.io/manifest.json'
@@ -177,11 +188,27 @@ class StremioService {
}
}
+ // Load addon order if exists
+ const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
+ if (storedOrder) {
+ this.addonOrder = JSON.parse(storedOrder);
+ // Filter out any ids that aren't in installedAddons
+ this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
+ }
+
+ // Add any missing addons to the order
+ const installedIds = Array.from(this.installedAddons.keys());
+ const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
+ this.addonOrder = [...this.addonOrder, ...missingIds];
+
// If no addons, install defaults
if (this.installedAddons.size === 0) {
await this.installDefaultAddons();
}
+ // Ensure order is saved
+ await this.saveAddonOrder();
+
this.initialized = true;
} catch (error) {
logger.error('Failed to initialize addons:', error);
@@ -245,6 +272,14 @@ class StremioService {
}
}
+ private async saveAddonOrder(): Promise {
+ try {
+ await AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder));
+ } catch (error) {
+ logger.error('Failed to save addon order:', error);
+ }
+ }
+
async getManifest(url: string): Promise {
try {
// Clean up URL - ensure it ends with manifest.json
@@ -278,7 +313,16 @@ class StremioService {
const manifest = await this.getManifest(url);
if (manifest && manifest.id) {
this.installedAddons.set(manifest.id, manifest);
+
+ // Add to order if not already present (new addons go to the end)
+ if (!this.addonOrder.includes(manifest.id)) {
+ this.addonOrder.push(manifest.id);
+ }
+
await this.saveInstalledAddons();
+ await this.saveAddonOrder();
+ // Emit an event that an addon was added
+ addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id);
} else {
throw new Error('Invalid addon manifest');
}
@@ -287,12 +331,20 @@ class StremioService {
removeAddon(id: string): void {
if (this.installedAddons.has(id)) {
this.installedAddons.delete(id);
+ // Remove from order
+ this.addonOrder = this.addonOrder.filter(addonId => addonId !== id);
this.saveInstalledAddons();
+ this.saveAddonOrder();
+ // Emit an event that an addon was removed
+ addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id);
}
}
getInstalledAddons(): Manifest[] {
- return Array.from(this.installedAddons.values());
+ // Return addons in the specified order
+ return this.addonOrder
+ .filter(id => this.installedAddons.has(id))
+ .map(id => this.installedAddons.get(id)!);
}
async getInstalledAddonsAsync(): Promise {
@@ -351,18 +403,23 @@ class StremioService {
// Add filters
if (filters.length > 0) {
+ logger.log(`Adding ${filters.length} filters to Cinemeta request`);
filters.forEach(filter => {
if (filter.value) {
+ logger.log(`Adding filter ${filter.title}=${filter.value}`);
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(filter.value)}`;
}
});
}
+ logger.log(`Cinemeta catalog request URL: ${url}`);
+
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
+ logger.log(`Cinemeta returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];
@@ -384,18 +441,23 @@ class StremioService {
// Add filters
if (filters.length > 0) {
+ logger.log(`Adding ${filters.length} filters to ${manifest.name} request`);
filters.forEach(filter => {
if (filter.value) {
+ logger.log(`Adding filter ${filter.title}=${filter.value}`);
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(filter.value)}`;
}
});
}
+ logger.log(`${manifest.name} catalog request URL: ${url}`);
+
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
+ logger.log(`${manifest.name} returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];
@@ -466,7 +528,7 @@ class StremioService {
}
}
- // Modify getStreams to use the new callback signature and rely on callbacks for results
+ // Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
async getStreams(type: string, id: string, callback?: StreamCallback): Promise {
await this.ensureInitialized();
@@ -783,6 +845,35 @@ class StremioService {
return [];
}
+
+ // Add methods to move addons in the order
+ moveAddonUp(id: string): boolean {
+ const index = this.addonOrder.indexOf(id);
+ if (index > 0) {
+ // Swap with the previous item
+ [this.addonOrder[index - 1], this.addonOrder[index]] =
+ [this.addonOrder[index], this.addonOrder[index - 1]];
+ this.saveAddonOrder();
+ // Emit an event that the order has changed
+ addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
+ return true;
+ }
+ return false;
+ }
+
+ moveAddonDown(id: string): boolean {
+ const index = this.addonOrder.indexOf(id);
+ if (index >= 0 && index < this.addonOrder.length - 1) {
+ // Swap with the next item
+ [this.addonOrder[index], this.addonOrder[index + 1]] =
+ [this.addonOrder[index + 1], this.addonOrder[index]];
+ this.saveAddonOrder();
+ // Emit an event that the order has changed
+ addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
+ return true;
+ }
+ return false;
+ }
}
export const stremioService = StremioService.getInstance();
diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts
index fb65552..a216196 100644
--- a/src/services/tmdbService.ts
+++ b/src/services/tmdbService.ts
@@ -790,6 +790,99 @@ export class TMDBService {
}
}
+ /**
+ * Get popular movies or TV shows
+ * @param type 'movie' or 'tv'
+ * @param page Page number for pagination
+ */
+ async getPopular(type: 'movie' | 'tv', page: number = 1): Promise {
+ try {
+ const response = await axios.get(`${BASE_URL}/${type}/popular`, {
+ headers: await this.getHeaders(),
+ params: await this.getParams({
+ language: 'en-US',
+ page,
+ }),
+ });
+
+ // Get external IDs for each popular item
+ const results = response.data.results || [];
+ const resultsWithExternalIds = await Promise.all(
+ results.map(async (item: TMDBTrendingResult) => {
+ try {
+ const externalIdsResponse = await axios.get(
+ `${BASE_URL}/${type}/${item.id}/external_ids`,
+ {
+ headers: await this.getHeaders(),
+ params: await this.getParams(),
+ }
+ );
+ return {
+ ...item,
+ external_ids: externalIdsResponse.data
+ };
+ } catch (error) {
+ logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
+ return item;
+ }
+ })
+ );
+
+ return resultsWithExternalIds;
+ } catch (error) {
+ logger.error(`Failed to get popular ${type} content:`, error);
+ return [];
+ }
+ }
+
+ /**
+ * Get upcoming/now playing content
+ * @param type 'movie' or 'tv'
+ * @param page Page number for pagination
+ */
+ async getUpcoming(type: 'movie' | 'tv', page: number = 1): Promise {
+ try {
+ // For movies use upcoming, for TV use on_the_air
+ const endpoint = type === 'movie' ? 'upcoming' : 'on_the_air';
+
+ const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, {
+ headers: await this.getHeaders(),
+ params: await this.getParams({
+ language: 'en-US',
+ page,
+ }),
+ });
+
+ // Get external IDs for each upcoming item
+ const results = response.data.results || [];
+ const resultsWithExternalIds = await Promise.all(
+ results.map(async (item: TMDBTrendingResult) => {
+ try {
+ const externalIdsResponse = await axios.get(
+ `${BASE_URL}/${type}/${item.id}/external_ids`,
+ {
+ headers: await this.getHeaders(),
+ params: await this.getParams(),
+ }
+ );
+ return {
+ ...item,
+ external_ids: externalIdsResponse.data
+ };
+ } catch (error) {
+ logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
+ return item;
+ }
+ })
+ );
+
+ return resultsWithExternalIds;
+ } catch (error) {
+ logger.error(`Failed to get upcoming ${type} content:`, error);
+ return [];
+ }
+ }
+
/**
* Get the list of official movie genres from TMDB
*/
@@ -825,6 +918,69 @@ export class TMDBService {
return [];
}
}
+
+ /**
+ * Discover movies or TV shows by genre
+ * @param type 'movie' or 'tv'
+ * @param genreName The genre name to filter by
+ * @param page Page number for pagination
+ */
+ async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1): Promise {
+ try {
+ // First get the genre ID from the name
+ const genreList = type === 'movie'
+ ? await this.getMovieGenres()
+ : await this.getTvGenres();
+
+ const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase());
+
+ if (!genre) {
+ logger.error(`Genre ${genreName} not found`);
+ return [];
+ }
+
+ const response = await axios.get(`${BASE_URL}/discover/${type}`, {
+ headers: await this.getHeaders(),
+ params: await this.getParams({
+ language: 'en-US',
+ sort_by: 'popularity.desc',
+ include_adult: false,
+ include_video: false,
+ page,
+ with_genres: genre.id.toString(),
+ with_original_language: 'en',
+ }),
+ });
+
+ // Get external IDs for each item
+ const results = response.data.results || [];
+ const resultsWithExternalIds = await Promise.all(
+ results.map(async (item: TMDBTrendingResult) => {
+ try {
+ const externalIdsResponse = await axios.get(
+ `${BASE_URL}/${type}/${item.id}/external_ids`,
+ {
+ headers: await this.getHeaders(),
+ params: await this.getParams(),
+ }
+ );
+ return {
+ ...item,
+ external_ids: externalIdsResponse.data
+ };
+ } catch (error) {
+ logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
+ return item;
+ }
+ })
+ );
+
+ return resultsWithExternalIds;
+ } catch (error) {
+ logger.error(`Failed to discover ${type} by genre ${genreName}:`, error);
+ return [];
+ }
+ }
}
export const tmdbService = TMDBService.getInstance();
diff --git a/src/services/traktService.ts b/src/services/traktService.ts
new file mode 100644
index 0000000..3030040
--- /dev/null
+++ b/src/services/traktService.ts
@@ -0,0 +1,462 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { logger } from '../utils/logger';
+
+// Storage keys
+export const TRAKT_ACCESS_TOKEN_KEY = 'trakt_access_token';
+export const TRAKT_REFRESH_TOKEN_KEY = 'trakt_refresh_token';
+export const TRAKT_TOKEN_EXPIRY_KEY = 'trakt_token_expiry';
+
+// Trakt API configuration
+const TRAKT_API_URL = 'https://api.trakt.tv';
+const TRAKT_CLIENT_ID = 'd7271f7dd57d8aeff63e99408610091a6b1ceac3b3a541d1031a48f429b7942c';
+const TRAKT_CLIENT_SECRET = '0abf42c39aaad72c74696fb5229b558a6ac4b747caf3d380d939e950e8a5449c';
+const TRAKT_REDIRECT_URI = 'stremioexpo://auth/trakt'; // This should match your registered callback URL
+
+// Types
+export interface TraktUser {
+ username: string;
+ name?: string;
+ private: boolean;
+ vip: boolean;
+ joined_at: string;
+ avatar?: string;
+}
+
+export interface TraktWatchedItem {
+ movie?: {
+ title: string;
+ year: number;
+ ids: {
+ trakt: number;
+ slug: string;
+ imdb: string;
+ tmdb: number;
+ };
+ };
+ show?: {
+ title: string;
+ year: number;
+ ids: {
+ trakt: number;
+ slug: string;
+ imdb: string;
+ tmdb: number;
+ };
+ };
+ plays: number;
+ last_watched_at: string;
+}
+
+export class TraktService {
+ private static instance: TraktService;
+ private accessToken: string | null = null;
+ private refreshToken: string | null = null;
+ private tokenExpiry: number = 0;
+ private isInitialized: boolean = false;
+
+ private constructor() {
+ // Initialization happens in initialize method
+ }
+
+ public static getInstance(): TraktService {
+ if (!TraktService.instance) {
+ TraktService.instance = new TraktService();
+ }
+ return TraktService.instance;
+ }
+
+ /**
+ * Initialize the Trakt service by loading stored tokens
+ */
+ public async initialize(): Promise {
+ if (this.isInitialized) {
+ return;
+ }
+
+ try {
+ const [accessToken, refreshToken, tokenExpiry] = await Promise.all([
+ AsyncStorage.getItem(TRAKT_ACCESS_TOKEN_KEY),
+ AsyncStorage.getItem(TRAKT_REFRESH_TOKEN_KEY),
+ AsyncStorage.getItem(TRAKT_TOKEN_EXPIRY_KEY)
+ ]);
+
+ this.accessToken = accessToken;
+ this.refreshToken = refreshToken;
+ this.tokenExpiry = tokenExpiry ? parseInt(tokenExpiry, 10) : 0;
+ this.isInitialized = true;
+
+ logger.log('[TraktService] Initialized, authenticated:', !!this.accessToken);
+ } catch (error) {
+ logger.error('[TraktService] Initialization failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Check if the user is authenticated with Trakt
+ */
+ public async isAuthenticated(): Promise {
+ await this.ensureInitialized();
+
+ if (!this.accessToken) {
+ return false;
+ }
+
+ // Check if token is expired and needs refresh
+ if (this.tokenExpiry && this.tokenExpiry < Date.now() && this.refreshToken) {
+ try {
+ await this.refreshAccessToken();
+ return !!this.accessToken;
+ } catch {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the authentication URL for Trakt OAuth
+ */
+ public getAuthUrl(): string {
+ return `https://trakt.tv/oauth/authorize?response_type=code&client_id=${TRAKT_CLIENT_ID}&redirect_uri=${encodeURIComponent(TRAKT_REDIRECT_URI)}`;
+ }
+
+ /**
+ * Exchange the authorization code for an access token
+ */
+ public async exchangeCodeForToken(code: string): Promise {
+ await this.ensureInitialized();
+
+ try {
+ const response = await fetch(`${TRAKT_API_URL}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ code,
+ client_id: TRAKT_CLIENT_ID,
+ client_secret: TRAKT_CLIENT_SECRET,
+ redirect_uri: TRAKT_REDIRECT_URI,
+ grant_type: 'authorization_code'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to exchange code: ${response.status}`);
+ }
+
+ const data = await response.json();
+ await this.saveTokens(data.access_token, data.refresh_token, data.expires_in);
+ return true;
+ } catch (error) {
+ logger.error('[TraktService] Failed to exchange code for token:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Refresh the access token using the refresh token
+ */
+ private async refreshAccessToken(): Promise {
+ if (!this.refreshToken) {
+ throw new Error('No refresh token available');
+ }
+
+ try {
+ const response = await fetch(`${TRAKT_API_URL}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ refresh_token: this.refreshToken,
+ client_id: TRAKT_CLIENT_ID,
+ client_secret: TRAKT_CLIENT_SECRET,
+ redirect_uri: TRAKT_REDIRECT_URI,
+ grant_type: 'refresh_token'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to refresh token: ${response.status}`);
+ }
+
+ const data = await response.json();
+ await this.saveTokens(data.access_token, data.refresh_token, data.expires_in);
+ } catch (error) {
+ logger.error('[TraktService] Failed to refresh token:', error);
+ await this.logout(); // Clear tokens if refresh fails
+ throw error;
+ }
+ }
+
+ /**
+ * Save authentication tokens to storage
+ */
+ private async saveTokens(accessToken: string, refreshToken: string, expiresIn: number): Promise {
+ this.accessToken = accessToken;
+ this.refreshToken = refreshToken;
+ this.tokenExpiry = Date.now() + (expiresIn * 1000);
+
+ try {
+ await AsyncStorage.multiSet([
+ [TRAKT_ACCESS_TOKEN_KEY, accessToken],
+ [TRAKT_REFRESH_TOKEN_KEY, refreshToken],
+ [TRAKT_TOKEN_EXPIRY_KEY, this.tokenExpiry.toString()]
+ ]);
+ logger.log('[TraktService] Tokens saved successfully');
+ } catch (error) {
+ logger.error('[TraktService] Failed to save tokens:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Log out the user by clearing all tokens
+ */
+ public async logout(): Promise {
+ await this.ensureInitialized();
+
+ try {
+ this.accessToken = null;
+ this.refreshToken = null;
+ this.tokenExpiry = 0;
+
+ await AsyncStorage.multiRemove([
+ TRAKT_ACCESS_TOKEN_KEY,
+ TRAKT_REFRESH_TOKEN_KEY,
+ TRAKT_TOKEN_EXPIRY_KEY
+ ]);
+ logger.log('[TraktService] Logged out successfully');
+ } catch (error) {
+ logger.error('[TraktService] Failed to logout:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Ensure the service is initialized before performing operations
+ */
+ private async ensureInitialized(): Promise {
+ if (!this.isInitialized) {
+ await this.initialize();
+ }
+ }
+
+ /**
+ * Make an authenticated API request to Trakt
+ */
+ private async apiRequest(
+ endpoint: string,
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
+ body?: any
+ ): Promise {
+ await this.ensureInitialized();
+
+ // Ensure we have a valid token
+ if (this.tokenExpiry && this.tokenExpiry < Date.now() && this.refreshToken) {
+ await this.refreshAccessToken();
+ }
+
+ if (!this.accessToken) {
+ throw new Error('Not authenticated');
+ }
+
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'trakt-api-version': '2',
+ 'trakt-api-key': TRAKT_CLIENT_ID,
+ 'Authorization': `Bearer ${this.accessToken}`
+ };
+
+ const options: RequestInit = {
+ method,
+ headers
+ };
+
+ if (body) {
+ options.body = JSON.stringify(body);
+ }
+
+ const response = await fetch(`${TRAKT_API_URL}${endpoint}`, options);
+
+ if (!response.ok) {
+ throw new Error(`API request failed: ${response.status}`);
+ }
+
+ return await response.json() as T;
+ }
+
+ /**
+ * Get the user's profile information
+ */
+ public async getUserProfile(): Promise {
+ return this.apiRequest('/users/me?extended=full');
+ }
+
+ /**
+ * Get the user's watched movies
+ */
+ public async getWatchedMovies(): Promise {
+ return this.apiRequest('/sync/watched/movies');
+ }
+
+ /**
+ * Get the user's watched shows
+ */
+ public async getWatchedShows(): Promise {
+ return this.apiRequest('/sync/watched/shows');
+ }
+
+ /**
+ * Get trakt id from IMDb id
+ */
+ public async getTraktIdFromImdbId(imdbId: string, type: 'movies' | 'shows'): Promise {
+ try {
+ const response = await fetch(`${TRAKT_API_URL}/search/${type}?id_type=imdb&id=${imdbId}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'trakt-api-version': '2',
+ 'trakt-api-key': TRAKT_CLIENT_ID
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to get Trakt ID: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data && data.length > 0) {
+ return data[0][type.slice(0, -1)].ids.trakt;
+ }
+ return null;
+ } catch (error) {
+ logger.error('[TraktService] Failed to get Trakt ID from IMDb ID:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Add a movie to user's watched history
+ */
+ public async addToWatchedMovies(imdbId: string, watchedAt: Date = new Date()): Promise {
+ try {
+ const traktId = await this.getTraktIdFromImdbId(imdbId, 'movies');
+ if (!traktId) {
+ return false;
+ }
+
+ await this.apiRequest('/sync/history', 'POST', {
+ movies: [
+ {
+ ids: {
+ trakt: traktId
+ },
+ watched_at: watchedAt.toISOString()
+ }
+ ]
+ });
+ return true;
+ } catch (error) {
+ logger.error('[TraktService] Failed to mark movie as watched:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Add a show episode to user's watched history
+ */
+ public async addToWatchedEpisodes(
+ imdbId: string,
+ season: number,
+ episode: number,
+ watchedAt: Date = new Date()
+ ): Promise {
+ try {
+ const traktId = await this.getTraktIdFromImdbId(imdbId, 'shows');
+ if (!traktId) {
+ return false;
+ }
+
+ await this.apiRequest('/sync/history', 'POST', {
+ episodes: [
+ {
+ ids: {
+ trakt: traktId
+ },
+ seasons: [
+ {
+ number: season,
+ episodes: [
+ {
+ number: episode,
+ watched_at: watchedAt.toISOString()
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ });
+ return true;
+ } catch (error) {
+ logger.error('[TraktService] Failed to mark episode as watched:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Check if a movie is in user's watched history
+ */
+ public async isMovieWatched(imdbId: string): Promise {
+ try {
+ if (!this.accessToken) {
+ return false;
+ }
+
+ const traktId = await this.getTraktIdFromImdbId(imdbId, 'movies');
+ if (!traktId) {
+ return false;
+ }
+
+ const response = await this.apiRequest(`/sync/history/movies/${traktId}`);
+ return response.length > 0;
+ } catch (error) {
+ logger.error('[TraktService] Failed to check if movie is watched:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Check if a show episode is in user's watched history
+ */
+ public async isEpisodeWatched(
+ imdbId: string,
+ season: number,
+ episode: number
+ ): Promise {
+ try {
+ if (!this.accessToken) {
+ return false;
+ }
+
+ const traktId = await this.getTraktIdFromImdbId(imdbId, 'shows');
+ if (!traktId) {
+ return false;
+ }
+
+ const response = await this.apiRequest(
+ `/sync/history/episodes/${traktId}?season=${season}&episode=${episode}`
+ );
+ return response.length > 0;
+ } catch (error) {
+ logger.error('[TraktService] Failed to check if episode is watched:', error);
+ return false;
+ }
+ }
+}
+
+// Export a singleton instance
+export const traktService = TraktService.getInstance();
\ No newline at end of file
diff --git a/src/utils/catalogNameUtils.ts b/src/utils/catalogNameUtils.ts
new file mode 100644
index 0000000..195cb38
--- /dev/null
+++ b/src/utils/catalogNameUtils.ts
@@ -0,0 +1,46 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { logger } from './logger';
+
+const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
+
+// Initialize cache as an empty object
+let customNamesCache: { [key: string]: string } = {};
+let cacheTimestamp: number = 0; // 0 indicates cache is invalid/empty initially
+const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
+
+async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
+ const now = Date.now();
+ // Check if cache is valid based on timestamp
+ if (cacheTimestamp > 0 && (now - cacheTimestamp < CACHE_DURATION)) {
+ return customNamesCache; // Cache is valid and guaranteed to be an object
+ }
+
+ try {
+ logger.info('Loading custom catalog names from storage...');
+ const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
+ // Assign parsed object or empty object if null/error
+ customNamesCache = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
+ cacheTimestamp = now; // Set timestamp only on successful load
+ return customNamesCache;
+ } catch (error) {
+ logger.error('Failed to load custom catalog names in utility:', error);
+ // Invalidate cache timestamp on error
+ cacheTimestamp = 0;
+ // Return the last known cache (which might be empty {}), or a fresh empty object
+ return customNamesCache || {}; // Return cache (could be outdated but non-null) or empty {}
+ }
+}
+
+export async function getCatalogDisplayName(addonId: string, type: string, catalogId: string, originalName: string): Promise {
+ // Ensure cache is loaded/refreshed before getting name
+ const customNames = await loadCustomNamesIfNeeded();
+ const key = `${addonId}:${type}:${catalogId}`;
+ return customNames[key] || originalName;
+}
+
+// Function to clear the cache if settings are updated elsewhere
+export function clearCustomNameCache() {
+ customNamesCache = {}; // Reset to empty object
+ cacheTimestamp = 0; // Invalidate timestamp
+ logger.info('Custom catalog name cache cleared.');
+}
\ No newline at end of file