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 | +| :----------------------------------------- | :----------------------------------------- | :--------------------------------------- | +| ![Home](src/assets/home.jpg) | ![Discover](src/assets/discover.jpg) | ![Search](src/assets/search.jpg) | +| **Metadata** | **Seasons & Episodes** | **Rating** | +| ![Metadata](src/assets/metadascreen.jpg) | ![Seasons](src/assets/seasonandepisode.jpg)| ![Rating](src/assets/ratingscreen.jpg) | -## 🛠️ 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 + + +