├── .babelrc ├── .buckconfig ├── .editorconfig ├── .env.dist ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── .watchmanconfig ├── .yarnrc ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── RELEASE.md ├── android ├── .gradle.properties.dist ├── app │ ├── _BUCK │ ├── app.keystore.enc │ ├── build.gradle │ ├── build_defs.bzl │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── holoplay │ │ │ └── ReactNativeFlipper.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ └── fonts │ │ │ ├── DINPro-Bold.ttf │ │ │ ├── DINPro-Medium.ttf │ │ │ └── DINPro-Regular.ttf │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ └── com │ │ │ └── holoplay │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── drawable-hdpi │ │ ├── favorite.png │ │ ├── headset.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backicon.png │ │ └── search.png │ │ ├── drawable-mdpi │ │ ├── favorite.png │ │ ├── headset.png │ │ ├── node_modules_reactnativepaper_src_assets_backchevron.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backicon.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backiconmask.png │ │ └── search.png │ │ ├── drawable-xhdpi │ │ ├── favorite.png │ │ ├── headset.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backicon.png │ │ └── search.png │ │ ├── drawable-xxhdpi │ │ ├── favorite.png │ │ ├── headset.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backicon.png │ │ └── search.png │ │ ├── drawable-xxxhdpi │ │ ├── favorite.png │ │ ├── headset.png │ │ ├── node_modules_reactnavigationstack_src_views_assets_backicon.png │ │ └── search.png │ │ ├── drawable │ │ ├── bootsplash.xml │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── bootsplash_logo.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── bootsplash_logo.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── bootsplash_logo.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── bootsplash_logo.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── bootsplash_logo.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.js ├── app.json ├── assets ├── bootsplash_logo.png ├── bootsplash_logo@1,5x.png ├── bootsplash_logo@2x.png ├── bootsplash_logo@3x.png ├── bootsplash_logo@4x.png └── bootsplash_logo_original.png ├── config ├── quickAction.ts └── theme.ts ├── docs └── logo.png ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ └── phoneScreenshots │ │ ├── dashboard-dark.jpg │ │ ├── dashboard.jpg │ │ ├── drawler.jpg │ │ ├── favoris.jpg │ │ ├── player.jpg │ │ ├── playlists.jpg │ │ ├── quick-actions.jpg │ │ ├── search.jpg │ │ └── settings.jpg │ ├── short_description.txt │ └── title.txt ├── index.js ├── metro.config.js ├── package.json ├── patches └── rn-fetch-blob+0.12.0.patch ├── scripts ├── android-run.sh ├── env-production.js └── git-push-tag.js ├── src ├── components │ ├── App │ │ └── index.tsx │ ├── AppPlayer │ │ └── index.tsx │ ├── BottomSheet │ │ └── index.tsx │ ├── Card │ │ ├── Layout │ │ │ └── index.tsx │ │ ├── List │ │ │ └── index.tsx │ │ ├── Playlist │ │ │ └── index.tsx │ │ ├── ScrollList │ │ │ └── index.tsx │ │ └── Search │ │ │ └── index.tsx │ ├── Carousel │ │ └── index.tsx │ ├── Data │ │ └── Empty │ │ │ └── index.tsx │ ├── Dialog │ │ ├── AddCustomInstance │ │ │ └── index.tsx │ │ ├── AddPlaylist │ │ │ └── index.tsx │ │ ├── AddVideoToPlaylist │ │ │ └── index.tsx │ │ ├── EditApiInstance │ │ │ └── index.tsx │ │ ├── EditToken │ │ │ └── index.tsx │ │ ├── EditUsername │ │ │ └── index.tsx │ │ ├── ErrorMonitoring │ │ │ └── index.tsx │ │ ├── Language │ │ │ └── index.tsx │ │ └── RemovePlaylist │ │ │ └── index.tsx │ ├── Dot │ │ └── index.tsx │ ├── Drawler │ │ └── index.tsx │ ├── ErrorBoundary │ │ └── index.tsx │ ├── Favoris │ │ ├── Button │ │ │ └── index.tsx │ │ └── List │ │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── Instance │ │ └── index.tsx │ ├── InstanceList │ │ └── index.tsx │ ├── Label │ │ └── index.tsx │ ├── LastPlays │ │ └── index.tsx │ ├── Layout │ │ └── index.tsx │ ├── LayoutSpacer │ │ └── index.tsx │ ├── Overlay │ │ └── index.tsx │ ├── Placeholder │ │ ├── Card │ │ │ └── index.tsx │ │ ├── CardCenter │ │ │ └── index.tsx │ │ └── Search │ │ │ └── index.tsx │ ├── Player │ │ └── index.tsx │ ├── PlayerSmall │ │ └── index.tsx │ ├── Playlist │ │ ├── List │ │ │ └── index.tsx │ │ └── Menu │ │ │ └── index.tsx │ ├── Profil │ │ └── index.tsx │ ├── Search │ │ ├── Bar │ │ │ └── index.tsx │ │ ├── BarAbsolute │ │ │ └── index.tsx │ │ ├── Empty │ │ │ └── index.tsx │ │ ├── Error │ │ │ └── index.tsx │ │ ├── PickerType │ │ │ └── index.tsx │ │ ├── Popular │ │ │ └── index.tsx │ │ ├── Result │ │ │ └── index.tsx │ │ ├── Submenu │ │ │ └── index.tsx │ │ └── Value │ │ │ └── index.tsx │ ├── Snackbar │ │ └── index.tsx │ ├── Spacer │ │ └── index.tsx │ ├── Version │ │ └── index.tsx │ └── Video │ │ └── index.tsx ├── constants │ └── index.ts ├── containers │ ├── CarouselSpacer │ │ └── index.ts │ ├── DialogAddVideoToPlaylist │ │ └── index.ts │ ├── Drawler │ │ └── index.ts │ ├── Favoris │ │ ├── Button │ │ │ └── index.ts │ │ └── Playlist │ │ │ └── index.ts │ ├── Instance │ │ └── index.ts │ ├── InstanceList │ │ └── index.ts │ ├── LastPlays │ │ └── index.ts │ ├── LayoutSpacer │ │ └── index.ts │ ├── Player │ │ └── index.ts │ ├── PlayerSmall │ │ └── index.ts │ ├── Playlist │ │ └── index.ts │ ├── Playlists │ │ ├── Carousel │ │ │ └── index.ts │ │ └── List │ │ │ └── index.ts │ ├── Profil │ │ └── index.ts │ ├── Search │ │ ├── Bar │ │ │ └── index.ts │ │ ├── BarAbsolute │ │ │ └── index.ts │ │ ├── PickerType │ │ │ └── index.ts │ │ ├── Popular │ │ │ └── index.ts │ │ ├── Result │ │ │ └── index.ts │ │ └── Value │ │ │ └── index.ts │ └── Snackbar │ │ └── index.ts ├── hooks │ ├── useBackup.ts │ ├── useDownloadFile.ts │ ├── useFavoris.ts │ ├── useInvidiousInstances.ts │ ├── useKeyboard.ts │ ├── useLinking.ts │ ├── usePlaylist.ts │ ├── useStore.ts │ ├── useUpdateRelease.ts │ └── useVideo.ts ├── i18n │ ├── cs.json │ ├── en.json │ ├── fr.json │ └── index.ts ├── queries │ └── search.ts ├── screens │ ├── Dashboard │ │ └── index.tsx │ ├── Favoris │ │ └── index.tsx │ ├── InvidiousInstances │ │ └── index.tsx │ ├── Loading │ │ └── index.tsx │ ├── Login │ │ ├── form.tsx │ │ └── index.tsx │ ├── Playlists │ │ └── index.tsx │ ├── PrivacyPolicy │ │ └── index.tsx │ ├── Search │ │ └── index.tsx │ └── Settings │ │ └── index.tsx ├── store │ ├── App │ │ └── index.ts │ ├── Data │ │ └── index.ts │ ├── Dialog │ │ └── index.ts │ ├── Player │ │ └── index.ts │ ├── Search │ │ └── index.ts │ ├── Snackbar │ │ └── index.ts │ ├── index.ts │ └── utils.ts ├── types │ ├── Api │ │ └── index.ts │ ├── QuickActions │ │ └── index.ts │ ├── Snackbar │ │ └── index.ts │ └── index.ts └── utils │ ├── ISO8601toDuration.ts │ ├── callApi.ts │ ├── downloadApk.ts │ ├── downloadFile.ts │ ├── fetchGithubAppVersion.ts │ ├── fetchInvidiousInstances.ts │ ├── fetchPlaylists.ts │ ├── formatTimeUnit.ts │ ├── getLanguageName.ts │ ├── hex2rgba.ts │ ├── invidiousSearch.ts │ ├── slugify.ts │ ├── stripTrailingSlash.ts │ └── youtubeDurationToSeconds.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:metro-react-native-babel-preset"], 3 | "env": { 4 | "production": { 5 | "plugins": ["react-native-paper/babel"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.ts] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.tsx] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.yml] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | [*.css] 29 | indent_style = space 30 | indent_size = 2 31 | 32 | [*.md] 33 | trim_trailing_whitespace = false 34 | 35 | [Makefile] 36 | indent_style = tab 37 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # Android App 2 | KEYSTORE_PASSWORD=TODO 3 | YOUTUBE_AUDIO_SERVER_API_URL=TODO 4 | 5 | # Android dependencies 6 | JAVA_HOME=/usr/lib/jvm/java-8-oracle 7 | ANDROID_HOME=$HOME/Android/Sdk 8 | 9 | # Sentry 10 | SENTRY_DSN_KEY=TODO 11 | 12 | # Github 13 | GITHUB_RELEASE=false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.story.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@react-native-community", 4 | "airbnb-typescript", 5 | "prettier", 6 | "prettier/@typescript-eslint", 7 | "prettier/react" 8 | ], 9 | "globals": { 10 | "__DEV__": true 11 | }, 12 | "rules": { 13 | "import/no-extraneous-dependencies": "off", 14 | "react/jsx-first-prop-new-line": [1, "multiline"], 15 | "react/jsx-max-props-per-line": [ 16 | 1, 17 | { 18 | "maximum": 1 19 | } 20 | ], 21 | "react/react-in-jsx-scope": "off", 22 | "react/prop-types": [0], 23 | "indent": [ 24 | 2, 25 | 2, 26 | { 27 | "SwitchCase": 1 28 | } 29 | ], 30 | "quotes": [2, "single"], 31 | "linebreak-style": [2, "unix"], 32 | "semi": [2, "always"], 33 | "no-console": [0], 34 | "no-loop-func": [0], 35 | "new-cap": [0], 36 | "no-trailing-spaces": [0], 37 | "no-param-reassign": [0], 38 | "func-names": [0], 39 | "comma-dangle": [0], 40 | "no-unused-expressions": [0], 41 | "block-scoped-var": [0], 42 | "jsx-quotes": ["error", "prefer-double"], 43 | "object-curly-spacing": ["error", "always"] 44 | }, 45 | "settings": { 46 | "react": { 47 | "pragma": "React", 48 | "version": "16.8.3" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .bash_history 3 | .env 4 | /.android 5 | *.log 6 | /.config 7 | .gradle 8 | .android 9 | .travis 10 | *.timestamp 11 | .npm 12 | .vscode 13 | 14 | # Core 15 | env.js 16 | dist 17 | 18 | # Mobile 19 | # OSX 20 | # 21 | .DS_Store 22 | 23 | # Xcode 24 | # 25 | build/ 26 | *.pbxuser 27 | !default.pbxuser 28 | *.mode1v3 29 | !default.mode1v3 30 | *.mode2v3 31 | !default.mode2v3 32 | *.perspectivev3 33 | !default.perspectivev3 34 | xcuserdata 35 | *.xccheckout 36 | *.moved-aside 37 | DerivedData 38 | *.hmap 39 | *.ipa 40 | *.xcuserstate 41 | project.xcworkspace 42 | 43 | # Android/IntelliJ 44 | # 45 | build/ 46 | .idea 47 | .gradle 48 | local.properties 49 | gradle.properties 50 | *.iml 51 | *.bundle 52 | 53 | # BUCK 54 | buck-out/ 55 | \.buckd/ 56 | *.keystore 57 | !debug.keystore 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/ 65 | 66 | */fastlane/report.xml 67 | */fastlane/Preview.html 68 | */fastlane/screenshots 69 | 70 | # Bundle artifact 71 | *.jsbundle 72 | 73 | # CocoaPods 74 | /ios/Pods/ 75 | 76 | TODO.md 77 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": true, 4 | "printWidth": 80, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "arrowParens": "avoid", 10 | "overrides": [ 11 | { 12 | "files": ["*.yaml", "*.yml"], 13 | "options": { 14 | "tabWidth": 4 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | dist: trusty 4 | 5 | android: 6 | components: 7 | - tools 8 | - platform-tools 9 | - build-tools-28.0.3 10 | - android-28 11 | - build-tools-29.0.3 12 | - android-29 13 | - extra 14 | 15 | before_install: 16 | - rm -rf android/app/build 17 | - openssl aes-256-cbc -K $encrypted_4562a431234b_key -iv $encrypted_4562a431234b_iv 18 | -in android/app/app.keystore.enc -out android/app/app.keystore -d 19 | - nvm install 12 20 | - npm i -g yarn 21 | - export PATH="$HOME/.yarn/bin:$PATH" 22 | before_script: 23 | - yarn install 24 | 25 | stages: 26 | - release 27 | 28 | jobs: 29 | include: 30 | - stage: release 31 | if: tag IS present 32 | name: Release app 33 | script: 34 | - make setup-production-env 35 | - make setup 36 | - make android-release 37 | 38 | deploy: 39 | provider: releases 40 | api_key: $GITHUB_TOKEN 41 | file: android/app/build/outputs/apk/release/app-release.apk 42 | skip_cleanup: true 43 | on: 44 | tags: true 45 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | lastUpdateCheck 1563028202236 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2020 HoloPlay 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # User ID 2 | USER_ID=`id -u` 3 | 4 | # include .env variables 5 | -include .env 6 | export $(shell sed 's/=.*//' .env) 7 | 8 | ANDROID_PATH = android 9 | 10 | # Help 11 | .SILENT: 12 | .PHONY: help 13 | 14 | help: ## Display this help 15 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 16 | 17 | 18 | ################# 19 | # Project Setup # 20 | ################# 21 | setup-production-env: 22 | @echo "--> Setup production env file" 23 | node ./scripts/env-production.js 24 | setup: 25 | @echo "--> Setup project env files" 26 | sed s/KEYSTORE_PASSWORD/$(KEYSTORE_PASSWORD)/g android/.gradle.properties.dist > android/gradle.properties 27 | 28 | 29 | ############## 30 | # Native App # 31 | ############## 32 | android-run: 33 | @echo "--> Run app on Android devices" 34 | yarn android:run 35 | android-release: 36 | @echo "--> Release Android App" 37 | yarn android:release 38 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # 🏗️ Improvements 2 | 3 | - Lorem ipsum 4 | 5 | # 🚀 Features 6 | 7 | - Lorem ipsum 8 | 9 | # 🐛 Bug Fixes 10 | 11 | - Lorem ipsum 12 | -------------------------------------------------------------------------------- /android/.gradle.properties.dist: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.37.0 29 | 30 | # Release 31 | MYAPP_RELEASE_STORE_FILE=app.keystore 32 | MYAPP_RELEASE_KEY_ALIAS=app-alias 33 | MYAPP_RELEASE_STORE_PASSWORD=KEYSTORE_PASSWORD 34 | MYAPP_RELEASE_KEY_PASSWORD=KEYSTORE_PASSWORD 35 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.holoplay", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.holoplay", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /android/app/app.keystore.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/app.keystore.enc -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/holoplay/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.holoplay; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/DINPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/assets/fonts/DINPro-Bold.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/DINPro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/assets/fonts/DINPro-Medium.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/DINPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/assets/fonts/DINPro-Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/java/com/holoplay/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.holoplay; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.facebook.react.ReactActivity; 6 | import com.zoontek.rnbootsplash.RNBootSplash; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "HoloPlay"; 17 | } 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | RNBootSplash.init(R.drawable.bootsplash, MainActivity.this); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/holoplay/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.holoplay; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.facebook.react.PackageList; 7 | import com.facebook.react.ReactApplication; 8 | import com.facebook.react.ReactInstanceManager; 9 | import com.lugg.ReactNativeConfig.ReactNativeConfigPackage; 10 | import com.facebook.react.ReactNativeHost; 11 | import com.facebook.react.ReactPackage; 12 | import com.facebook.react.shell.MainReactPackage; 13 | import com.facebook.soloader.SoLoader; 14 | import java.lang.reflect.InvocationTargetException; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | 19 | public class MainApplication extends Application implements ReactApplication { 20 | 21 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 22 | 23 | @Override 24 | public boolean getUseDeveloperSupport() { 25 | return BuildConfig.DEBUG; 26 | } 27 | 28 | @Override 29 | protected List getPackages() { 30 | @SuppressWarnings("UnnecessaryLocalVariable") 31 | List packages = new PackageList(this).getPackages(); 32 | // Packages that cannot be autolinked yet can be added manually here, for example: 33 | // packages.add(new MyReactNativePackage()); 34 | return packages; 35 | } 36 | 37 | @Override 38 | protected String getJSMainModuleName() { 39 | return "index"; 40 | } 41 | }; 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | return mReactNativeHost; 46 | } 47 | 48 | public void onCreate() { 49 | super.onCreate(); 50 | SoLoader.init(this, /* native exopackage */ false); 51 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 52 | } 53 | /** 54 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 55 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 56 | * 57 | * @param context 58 | */ 59 | private static void initializeFlipper( 60 | Context context, ReactInstanceManager reactInstanceManager) { 61 | if (BuildConfig.DEBUG) { 62 | try { 63 | /* 64 | We use reflection here to pick up the class that initializes Flipper, 65 | since Flipper library is not available in release mode 66 | */ 67 | Class aClass = Class.forName("com.rndiffapp.ReactNativeFlipper"); 68 | aClass 69 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 70 | .invoke(null, context, reactInstanceManager); 71 | } catch (ClassNotFoundException e) { 72 | e.printStackTrace(); 73 | } catch (NoSuchMethodException e) { 74 | e.printStackTrace(); 75 | } catch (IllegalAccessException e) { 76 | e.printStackTrace(); 77 | } catch (InvocationTargetException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-hdpi/favorite.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/headset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-hdpi/headset.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-hdpi/search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/favorite.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/headset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/headset.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/node_modules_reactnativepaper_src_assets_backchevron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/node_modules_reactnativepaper_src_assets_backchevron.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-mdpi/search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xhdpi/favorite.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/headset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xhdpi/headset.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xhdpi/search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxhdpi/favorite.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/headset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxhdpi/headset.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxhdpi/search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxxhdpi/favorite.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/headset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxxhdpi/headset.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/drawable-xxxhdpi/search.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/bootsplash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #FFFFFF 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HoloPlay 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.3" 6 | minSdkVersion = 21 7 | compileSdkVersion = 29 8 | targetSdkVersion = 29 9 | ndkVersion = "20.1.5948944" 10 | } 11 | repositories { 12 | google() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath("com.android.tools.build:gradle:4.1.0") 17 | 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | mavenLocal() 26 | maven { 27 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 28 | url("$rootDir/../node_modules/react-native/android") 29 | } 30 | maven { 31 | // Android JSC is installed from npm 32 | url("$rootDir/../node_modules/jsc-android/dist") 33 | } 34 | 35 | google() 36 | jcenter() 37 | maven { url 'https://www.jitpack.io' } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'HoloPlay' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':react-native-video' 4 | project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer') 5 | include ':app' 6 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import config from 'react-native-config'; 3 | import QuickActions from 'react-native-quick-actions'; 4 | import React, { useEffect } from 'react'; 5 | import * as Sentry from '@sentry/react-native'; 6 | import { Provider } from './src/store'; 7 | import App from './src/components/App'; 8 | import { quickActionShortcutItems } from './config/quickAction'; 9 | import { Button } from 'react-native-paper'; 10 | import useStore from './src/hooks/useStore'; 11 | 12 | QuickActions.isSupported((error, supported) => { 13 | if (supported) { 14 | return QuickActions.setShortcutItems(quickActionShortcutItems); 15 | } 16 | 17 | return error; 18 | }); 19 | 20 | export default () => { 21 | const store = useStore(); 22 | 23 | if (store.sendErrorMonitoring) { 24 | Sentry.init({ 25 | dsn: config.SENTRY_DSN_KEY 26 | }); 27 | } 28 | 29 | return ( 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HoloPlay", 3 | "displayName": "HoloPlay" 4 | } -------------------------------------------------------------------------------- /assets/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo.png -------------------------------------------------------------------------------- /assets/bootsplash_logo@1,5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo@1,5x.png -------------------------------------------------------------------------------- /assets/bootsplash_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo@2x.png -------------------------------------------------------------------------------- /assets/bootsplash_logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo@3x.png -------------------------------------------------------------------------------- /assets/bootsplash_logo@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo@4x.png -------------------------------------------------------------------------------- /assets/bootsplash_logo_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/assets/bootsplash_logo_original.png -------------------------------------------------------------------------------- /config/quickAction.ts: -------------------------------------------------------------------------------- 1 | export const QUICK_ACTION_PLAYLISTS = 'Playlists'; 2 | export const QUICK_ACTION_FAVORIS = 'Favoris'; 3 | export const QUICK_ACTION_SEARCH = 'Search'; 4 | 5 | export const quickActionShortcutItems = [ 6 | { 7 | type: 'Orders', 8 | title: QUICK_ACTION_PLAYLISTS, 9 | icon: 'headset', 10 | userInfo: { 11 | url: 'app://playlists' 12 | } 13 | }, 14 | { 15 | type: 'Orders', 16 | title: QUICK_ACTION_FAVORIS, 17 | icon: 'favorite', 18 | userInfo: { 19 | url: 'app://favoris' 20 | } 21 | }, 22 | { 23 | type: 'Orders', 24 | title: QUICK_ACTION_SEARCH, 25 | icon: 'search', 26 | userInfo: { 27 | url: 'app://search' 28 | } 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /config/theme.ts: -------------------------------------------------------------------------------- 1 | import { DarkTheme, DefaultTheme } from 'react-native-paper'; 2 | 3 | export const DASHBOARD_COLOR = '#2575f4'; 4 | export const SEARCH_COLOR = '#558B2F'; 5 | export const PLAYLISTS_COLOR = '#fe5f55'; 6 | export const FAVORIS_COLOR = '#EE05F2'; 7 | 8 | export const darkTheme = { 9 | ...DarkTheme, 10 | colors: { 11 | ...DarkTheme.colors, 12 | background: '#111111', 13 | surface: '#1d1d1d', 14 | favoris: DarkTheme.colors.accent, 15 | fabGroup: DarkTheme.colors.accent, 16 | screens: { 17 | dashboard: '#2d2d2d', 18 | search: '#2d2d2d', 19 | playlists: '#2d2d2d', 20 | favoris: '#2d2d2d' 21 | } 22 | } 23 | }; 24 | 25 | export const defaultTheme = { 26 | ...DefaultTheme, 27 | colors: { 28 | ...DefaultTheme.colors, 29 | primary: DASHBOARD_COLOR, 30 | background: '#f5f6f9', 31 | favoris: FAVORIS_COLOR, 32 | fabGroup: PLAYLISTS_COLOR, 33 | screens: { 34 | dashboard: DASHBOARD_COLOR, 35 | search: SEARCH_COLOR, 36 | playlists: PLAYLISTS_COLOR, 37 | favoris: FAVORIS_COLOR 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/docs/logo.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | HoloPlay is a audio only alternative Youtube app using Invidious API. You can add your Invidious token and save music to favoris or create your playlists. 2 | 3 | Features : 4 | 5 | - Search by video and playlist 6 | - Live video 7 | - Create your playlists 8 | - Save on favoris 9 | - Downloading video 10 | - Background mode 11 | - Offline 12 | - Work on Android Auto 13 | - Respect your privacy 14 | - Open Source 15 | - Cloud Syncing 16 | - Dark Theme 17 | - internationalization with EN (default) and FR 18 | - Add your self hosted Invidious instance URL 19 | 20 | HoloPlay is an open source project and can be found at https://github.com/stephane-r/HoloPlay and https://holoplay.io. 21 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/dashboard-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/dashboard-dark.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/dashboard.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/drawler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/drawler.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/favoris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/favoris.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/player.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/playlists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/playlists.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/quick-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/quick-actions.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/search.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephane-r/HoloPlay/dcab76c19173ba62438c4f12cf939f8acf5529a9/fastlane/metadata/android/en-US/images/phoneScreenshots/settings.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | HoloPlay is a audio only alternative Youtube client using Invidious API. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | HoloPlay 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import { setConsole } from 'react-query'; 3 | import './src/i18n'; 4 | import App from './app'; 5 | import { name as appName } from './app.json'; 6 | 7 | setConsole({ 8 | log: console.log, 9 | warn: console.warn, 10 | error: console.warn 11 | }); 12 | 13 | AppRegistry.registerComponent(appName, () => App); 14 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true 14 | } 15 | }) 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "holoplay", 3 | "version": "1.12.2", 4 | "private": false, 5 | "scripts": { 6 | "postinstall": "npx patch-package", 7 | "postversion": "RNV=android react-native-version", 8 | "start": "react-native start -- --reset-cache", 9 | "android:run": "./scripts/android-run.sh", 10 | "android:release": "cd android && ./gradlew assembleRelease", 11 | "android:release:debug": "cd android && ./gradlew assembleDebug", 12 | "version:major": "node ./scripts/git-push-tag major", 13 | "version:minor": "node ./scripts/git-push-tag minor", 14 | "version:patch": "node ./scripts/git-push-tag patch", 15 | "clean:log": "rm *.log", 16 | "prettify": "prettier --write src/**/*.js", 17 | "precommit-msg": "echo 'Pre-commit checks...' && exit 0", 18 | "lint": "eslint src", 19 | "static-analysis": "yarn lint" 20 | }, 21 | "resolutions": { 22 | "**/**/node-fetch": "2.6.1" 23 | }, 24 | "dependencies": { 25 | "@react-native-community/async-storage": "^1.12.1", 26 | "@react-native-community/masked-view": "^0.1.11", 27 | "@react-native-community/picker": "^1.8.1", 28 | "@react-native-community/slider": "^3.0.3", 29 | "@react-native-community/viewpager": "^5.0.11", 30 | "@react-navigation/compat": "^5.3.15", 31 | "@react-navigation/material-bottom-tabs": "^5.3.15", 32 | "@react-navigation/native": "^5.9.4", 33 | "@react-navigation/stack": "^5.14.5", 34 | "@sentry/react-native": "^2.4.3", 35 | "dotenv": "^10.0.0", 36 | "hh-mm-ss": "^1.2.0", 37 | "i18next": "^20.3.1", 38 | "react": "17.0.1", 39 | "react-i18next": "^11.10.0", 40 | "react-native": "0.64.2", 41 | "react-native-animation-hooks": "^1.0.1", 42 | "react-native-bootsplash": "^3.2.3", 43 | "react-native-config": "^1.4.2", 44 | "react-native-draggable-flatlist": "^2.6.2", 45 | "react-native-fs": "^2.18.0", 46 | "react-native-gesture-handler": "^1.10.3", 47 | "react-native-get-random-values": "^1.7.0", 48 | "react-native-linear-gradient": "^2.5.6", 49 | "react-native-music-control": "^1.4.0", 50 | "react-native-paper": "^4.9.1", 51 | "react-native-quick-actions": "^0.3.13", 52 | "react-native-reanimated": "^2.2.0", 53 | "react-native-safe-area-context": "^3.2.0", 54 | "react-native-screens": "^3.3.0", 55 | "react-native-snap-carousel": "^3.9.1", 56 | "react-native-svg": "^12.1.1", 57 | "react-native-svg-icon": "^0.10.0", 58 | "react-native-vector-icons": "^8.1.0", 59 | "react-native-version": "^4.0.0", 60 | "react-native-video": "^5.1.1", 61 | "react-query": "2", 62 | "react-waterfall": "^4.0.4", 63 | "rn-dominant-color": "^1.7.2", 64 | "rn-fetch-blob": "^0.12.0", 65 | "rn-placeholder": "^3.0.3", 66 | "semver-compare": "^1.0.0", 67 | "shelljs": "^0.8.5", 68 | "uuid": "^8.3.2" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.12.9", 72 | "@babel/runtime": "^7.12.5", 73 | "@react-native-community/eslint-config": "^2.0.0", 74 | "@types/hh-mm-ss": "^1.2.1", 75 | "@types/react": "^17.0.8", 76 | "@types/react-native": "^0.64.8", 77 | "@types/react-native-snap-carousel": "^3.8.3", 78 | "@types/react-native-video": "^5.0.5", 79 | "@typescript-eslint/eslint-plugin": "^4.25.0", 80 | "babel-eslint": "^10.0.1", 81 | "babel-jest": "^26.6.3", 82 | "eslint": "7.14.0", 83 | "eslint-config-airbnb-base": "^14.2.1", 84 | "eslint-config-airbnb-typescript": "^12.3.1", 85 | "eslint-config-prettier": "^8.3.0", 86 | "eslint-plugin-import": "^2.23.4", 87 | "eslint-plugin-jsx-a11y": "^6.4.1", 88 | "eslint-plugin-prettier": "^3.4.0", 89 | "eslint-plugin-react": "^7.24.0", 90 | "jest": "^26.6.3", 91 | "metro-react-native-babel-preset": "^0.64.0", 92 | "pre-commit": "^1.2.2", 93 | "prettier": "^2.3.0", 94 | "react-test-renderer": "17.0.1", 95 | "typescript": "^3.9.5" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /scripts/android-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | JAVA_HOME=$(grep JAVA_HOME .env | cut -d '=' -f2) 3 | ANDROID_HOME=$(grep ANDROID_HOME .env | cut -d '=' -f2) 4 | 5 | rm -rf .cache 6 | export JAVA_HOME 7 | export ANDROID_HOME 8 | export PATH=${PATH}:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools 9 | 10 | node ./node_modules/react-native/local-cli/cli.js run-android 11 | -------------------------------------------------------------------------------- /scripts/env-production.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | const envs = [ 4 | { 5 | value: 'SENTRY_DSN_KEY', 6 | property: process.env.SENTRY_DSN_KEY 7 | }, 8 | { 9 | value: 'YOUTUBE_AUDIO_SERVER_API_URL', 10 | property: process.env.YOUTUBE_AUDIO_SERVER_API_URL 11 | }, 12 | { 13 | value: 'GITHUB_RELEASE', 14 | property: process.env.GITHUB_RELEASE 15 | } 16 | ]; 17 | 18 | envs.forEach(({ value, property }) => 19 | shell.exec(`echo ${value}=${property} >> .env`) 20 | ); 21 | -------------------------------------------------------------------------------- /scripts/git-push-tag.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | shell.exec(`npm version ${process.argv.slice(2)[0]}`); 4 | 5 | const { version } = require('../package.json'); 6 | 7 | shell.exec(`git tag ${version}`); 8 | shell.exec(`git push origin ${version}`); 9 | shell.exec('git push origin develop'); 10 | shell.exec('echo New tag pushed.'); 11 | -------------------------------------------------------------------------------- /src/components/AppPlayer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef } from 'react'; 2 | import { Animated, Dimensions, View } from 'react-native'; 3 | import PlayerContainer from '../../containers/Player'; 4 | import BottomSheet from '../BottomSheet'; 5 | import PlayerSmallContainer from '../../containers/PlayerSmall'; 6 | import Overlay from '../Overlay'; 7 | 8 | const AppPlayer: React.FC = () => { 9 | const bottomSheet = useRef(null); 10 | const opacity = new Animated.Value(0); 11 | 12 | const showPlayer = (): void => { 13 | Animated.timing(opacity, { 14 | toValue: 1, 15 | duration: 400, 16 | useNativeDriver: false 17 | }).start(); 18 | bottomSheet.current.show(); 19 | }; 20 | 21 | const onClose = (): void => 22 | Animated.timing(opacity, { 23 | toValue: 0, 24 | duration: 400, 25 | useNativeDriver: false 26 | }).start(); 27 | 28 | return ( 29 | <> 30 | 31 | 32 | 36 | bottomSheet.current.close()} /> 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default memo(AppPlayer); 43 | -------------------------------------------------------------------------------- /src/components/BottomSheet/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect, PureComponent } from 'react'; 2 | import { 3 | View, 4 | TouchableOpacity, 5 | Animated, 6 | PanResponder, 7 | StyleSheet, 8 | Dimensions, 9 | Alert 10 | } from 'react-native'; 11 | import PlayerContainer from '../../containers/Player'; 12 | import { Text } from 'react-native-paper'; 13 | 14 | class BottomSheet extends PureComponent { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | modalVisible: false, 19 | animatedHeight: new Animated.Value(0), 20 | pan: new Animated.ValueXY() 21 | }; 22 | 23 | this.createPanResponder(props); 24 | } 25 | 26 | setModalVisible(visible) { 27 | const { 28 | closeFunction, 29 | height, 30 | hasDragabbleIcon, 31 | backgroundColor, 32 | dragIconColor 33 | } = this.props; 34 | const { animatedHeight, pan } = this.state; 35 | if (visible) { 36 | this.setState({ modalVisible: visible }); 37 | Animated.timing(animatedHeight, { 38 | toValue: height, 39 | duration: 300, 40 | useNativeDriver: false 41 | }).start(); 42 | } else { 43 | Animated.timing(animatedHeight, { 44 | toValue: 0, 45 | duration: 400, 46 | useNativeDriver: false 47 | }).start(() => { 48 | pan.setValue({ x: 0, y: 0 }); 49 | this.setState({ 50 | modalVisible: visible, 51 | animatedHeight: new Animated.Value(0) 52 | }); 53 | if (typeof closeFunction === 'function') closeFunction(); 54 | }); 55 | this.props.onClose(); 56 | } 57 | } 58 | 59 | createPanResponder(props) { 60 | const { height, draggable = true } = props; 61 | const { pan } = this.state; 62 | 63 | this.panResponder = PanResponder.create({ 64 | onStartShouldSetPanResponder: () => true, 65 | onPanResponderMove: (e, gestureState) => { 66 | if (draggable && gestureState.dy > 0) { 67 | Animated.event([null, { dy: pan.y }], { useNativeDriver: false })( 68 | e, 69 | gestureState 70 | ); 71 | } 72 | }, 73 | onPanResponderRelease: (e, gestureState) => { 74 | const gestureLimitArea = height / 3; 75 | const gestureDistance = gestureState.dy; 76 | if (draggable && gestureDistance > gestureLimitArea) { 77 | this.setModalVisible(false); 78 | } else { 79 | Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start(); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | show() { 86 | this.setModalVisible(true); 87 | } 88 | 89 | close() { 90 | this.setModalVisible(false); 91 | } 92 | 93 | render() { 94 | const { 95 | children, 96 | hasDraggableIcon, 97 | backgroundColor, 98 | dragIconColor 99 | } = this.props; 100 | const { animatedHeight, pan, modalVisible } = this.state; 101 | const panStyle = { 102 | transform: pan.getTranslateTransform() 103 | }; 104 | 105 | return ( 106 | 107 | this.close()} 111 | /> 112 | 115 | {children} 116 | 117 | 118 | ); 119 | } 120 | } 121 | 122 | const styles = StyleSheet.create({ 123 | wrapper: { 124 | position: 'absolute', 125 | width: '100%', 126 | bottom: 0 127 | }, 128 | background: { 129 | flex: 1, 130 | backgroundColor: 'transparent' 131 | }, 132 | container: { 133 | backgroundColor: '#F3F3F3', 134 | width: '100%', 135 | height: 0, 136 | overflow: 'hidden' 137 | }, 138 | draggableContainer: { 139 | width: '100%', 140 | alignItems: 'center', 141 | backgroundColor: 'transparent' 142 | } 143 | }); 144 | 145 | export default BottomSheet; 146 | -------------------------------------------------------------------------------- /src/components/Card/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | 4 | const CardList: React.FC = ({ children }) => ( 5 | {children} 6 | ); 7 | 8 | const styles = StyleSheet.create({ 9 | list: { 10 | flexDirection: 'row', 11 | flexWrap: 'wrap', 12 | paddingTop: 23, 13 | marginHorizontal: -8 14 | } 15 | }); 16 | 17 | export default CardList; 18 | -------------------------------------------------------------------------------- /src/components/Card/ScrollList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Dimensions } from 'react-native'; 3 | import { ScrollView } from 'react-native-gesture-handler'; 4 | 5 | const CardScrollList: React.FC = ({ children }) => ( 6 | 7 | {children} 8 | 9 | ); 10 | 11 | const styles = StyleSheet.create({ 12 | list: { 13 | paddingTop: 23, 14 | marginHorizontal: -8 15 | } 16 | }); 17 | 18 | export default CardScrollList; 19 | -------------------------------------------------------------------------------- /src/components/Card/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Alert } from 'react-native'; 3 | import timeFormat from 'hh-mm-ss'; 4 | import Card from '../Layout'; 5 | import FavorisButtonContainer from '../../../containers/Favoris/Button'; 6 | import { actions } from '../../../store'; 7 | import { SearchVideo, Video } from '../../../types'; 8 | import { IconButton } from 'react-native-paper'; 9 | 10 | interface Props { 11 | video: SearchVideo; 12 | setPlaylistFrom: string; 13 | loopIndex?: number; 14 | favorisButtonColor: null | string; 15 | containerCustomStyle?: { 16 | [key: string]: string | number; 17 | }; 18 | pictureCustomStyle?: { 19 | [key: string]: string | number; 20 | }; 21 | } 22 | 23 | const CardSearch: React.FC = ({ 24 | video, 25 | setPlaylistFrom, 26 | loopIndex, 27 | favorisButtonColor = null, 28 | ...props 29 | }) => { 30 | const [loading, setLoading] = useState(false); 31 | 32 | const loadVideo = async (index: number): Promise => { 33 | try { 34 | setLoading(true); 35 | 36 | if (video.liveNow) { 37 | await actions.loadLiveVideo({ 38 | videoIndex: index, 39 | data: video, 40 | setPlaylistFrom 41 | }); 42 | } else { 43 | await actions.loadVideo({ videoIndex: index, setPlaylistFrom }); 44 | } 45 | } catch (error) { 46 | actions.setSnackbar({ 47 | message: error.message 48 | }); 49 | } finally { 50 | setLoading(false); 51 | } 52 | }; 53 | 54 | const loadPlaylistVideo = async (): Promise => { 55 | try { 56 | setLoading(true); 57 | await actions.loadPlaylist(video.playlistId); 58 | await actions.loadVideo({ videoIndex: loopIndex, setPlaylistFrom }); 59 | } catch (error) { 60 | actions.setSnackbar({ 61 | message: error.message 62 | }); 63 | } finally { 64 | setLoading(false); 65 | } 66 | }; 67 | 68 | const card = { 69 | title: video.title, 70 | picture: 71 | video.videoThumbnails?.find(q => q.quality === 'medium').url || 72 | video?.playlistThumbnail, 73 | duration: 74 | video.type === 'playlist' 75 | ? `${video.videos.length} videos` 76 | : video.lengthSeconds 77 | ? timeFormat.fromS(video?.lengthSeconds) 78 | : null, 79 | liveNow: video.liveNow 80 | }; 81 | 82 | return ( 83 | { 86 | if (video.type === 'playlist') { 87 | return loadPlaylistVideo(); 88 | } 89 | 90 | return loadVideo(video.index ?? loopIndex); 91 | }} 92 | alignment="vertical" 93 | isLoading={loading} 94 | {...props}> 95 | {video.type !== 'playlist' && ( 96 | 104 | 109 | actions.setVideoDialogAddVideoToPlaylist(video)} 114 | /> 115 | 116 | )} 117 | 118 | ); 119 | }; 120 | 121 | export default CardSearch; 122 | -------------------------------------------------------------------------------- /src/components/Carousel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { View, Dimensions, StyleSheet } from 'react-native'; 3 | import { IconButton, Text } from 'react-native-paper'; 4 | import SnapCarousel from 'react-native-snap-carousel'; 5 | import Card from '../Card/Layout'; 6 | import { actions } from '../../store'; 7 | import { Playlist } from '../../types'; 8 | import Spacer from '../Spacer'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | type CarouselPlayIconProps = { 12 | onPress?: () => void; 13 | }; 14 | 15 | const CarouselPlayIcon: React.FC = ({ onPress }) => ( 16 | 26 | ); 27 | 28 | interface CarouselItemProps { 29 | item: Playlist; 30 | index: number; 31 | } 32 | 33 | const setCardItem = (item: any): any => ({ 34 | title: item.title, 35 | picture: 36 | item.videos[0]?.videoThumbnails[0]?.url ?? 37 | 'https://greeneyedmedia.com/wp-content/plugins/woocommerce/assets/images/placeholder.png' 38 | }); 39 | 40 | const CarouselItem: React.FC = ({ item, t }) => { 41 | const videosCount = item.videos?.length ?? 0; 42 | 43 | const runPlaylist = async (): Promise => 44 | actions.loadVideo({ videoIndex: 0, setPlaylistFrom: item.videos }); 45 | 46 | return ( 47 | 48 | 56 | videosCount > 0 ? runPlaylist() : null 57 | } 58 | /> 59 | }> 60 | 61 | {videosCount} {t('playlists.song')} 62 | {videosCount > 1 && 's'} 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | interface CarouselProps { 70 | playlists: Playlist[]; 71 | } 72 | 73 | const Carousel: React.FC = ({ playlists }) => { 74 | const { t } = useTranslation(); 75 | 76 | return ( 77 | 78 | } 85 | /> 86 | 87 | ); 88 | }; 89 | 90 | const styles = StyleSheet.create({ 91 | itemContainer: { 92 | paddingTop: 20 93 | } 94 | }); 95 | 96 | export { CarouselPlayIcon, setCardItem }; 97 | export default memo(Carousel); 98 | -------------------------------------------------------------------------------- /src/components/Data/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | // @ts-ignore 4 | import { Text, useTheme } from 'react-native-paper'; 5 | 6 | interface Props { 7 | text: string; 8 | } 9 | 10 | const DataEmpty: React.FC = ({ text, children }) => { 11 | const { colors } = useTheme(); 12 | 13 | return ( 14 | 21 | {text && {text}} 22 | {children && children} 23 | 24 | ); 25 | }; 26 | 27 | const styles = StyleSheet.create({ 28 | container: { 29 | padding: 16, 30 | marginBottom: 20, 31 | elevation: 2, 32 | borderRadius: 4, 33 | minHeight: 80 34 | } 35 | }); 36 | 37 | export default DataEmpty; 38 | -------------------------------------------------------------------------------- /src/components/Dialog/AddCustomInstance/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Button, TextInput, useTheme, Text } from 'react-native-paper'; 3 | import { Picker } from '@react-native-community/picker'; 4 | import { ApiRoutes } from '../../../constants'; 5 | import { View, Alert } from 'react-native'; 6 | import { actions } from '../../../store'; 7 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 8 | import callApi from '../../../utils/callApi'; 9 | import useInvidiousInstances from '../../../hooks/useInvidiousInstances'; 10 | import { useTranslation } from 'react-i18next'; 11 | import useStore from '../../../hooks/useStore'; 12 | import stripTrailingSlash from '../../../utils/stripTrailingSlash'; 13 | import Spacer from '../../Spacer'; 14 | 15 | interface Props { 16 | visible: boolean; 17 | onDismiss: () => void; 18 | } 19 | 20 | const DialogAddCustomInstance: React.FC = ({ visible, onDismiss }) => { 21 | const store = useStore(); 22 | const [uri, setUri] = useState('https://'); 23 | const { t } = useTranslation(); 24 | 25 | const IS_NOT_VALID_URI = !/(http(s?)):\/\//i.test(uri); 26 | 27 | const onValueChange = (value: string): void => setUri(true); 28 | 29 | const submit = async () => { 30 | try { 31 | actions.setCustomInstance({ 32 | isCustom: true, 33 | uri 34 | }); 35 | 36 | return setTimeout( 37 | () => 38 | actions.setSnackbar({ 39 | message: t('snackbar.addCustomInstanceSuccess') 40 | }), 41 | 500 42 | ); 43 | } catch (error) { 44 | return setTimeout( 45 | () => 46 | actions.setSnackbar({ 47 | message: t('snackbar.invidiousInstanceTokenUpdated') 48 | }), 49 | 500 50 | ); 51 | } finally { 52 | onDismiss(); 53 | } 54 | }; 55 | 56 | return ( 57 |

{ 60 | setUri(null); 61 | onDismiss(); 62 | }}> 63 | {t('dialog.customInstance.title')} 64 | 65 | 66 | {t('dialog.customInstance.example')} :{' '} 67 | 68 | https://my-custom-invidious.com 69 | 70 | 71 | 72 | 79 | 80 | 81 | 82 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default DialogAddCustomInstance; 91 | -------------------------------------------------------------------------------- /src/components/Dialog/AddPlaylist/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Portal, Button, Dialog, TextInput } from 'react-native-paper'; 3 | import { actions } from '../../../store'; 4 | import useStore from '../../../hooks/useStore'; 5 | import { Playlist } from '../../../types'; 6 | import callApi from '../../../utils/callApi'; 7 | import { ApiRoutes } from '../../../constants'; 8 | import { Alert } from 'react-native'; 9 | import usePlaylist from '../../../hooks/usePlaylist'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | interface Props { 13 | toggleDialog: (value: null | Playlist) => void; 14 | visible: boolean; 15 | playlist: Playlist; 16 | } 17 | 18 | export const playlistProps = { 19 | title: '', 20 | privacy: 'public' 21 | }; 22 | 23 | const DialogAddPlaylist: React.FC = ({ 24 | toggleDialog, 25 | visible, 26 | ...props 27 | }) => { 28 | const store = useStore(); 29 | const [loading, setLoading] = useState(false); 30 | const [playlist, setPlaylist] = useState(props.playlist ?? playlistProps); 31 | const { createPlaylist, updatePlaylist } = usePlaylist(); 32 | const { t } = useTranslation(); 33 | 34 | useEffect(() => { 35 | if (props.playlist) { 36 | setPlaylist(props.playlist); 37 | } 38 | }, [props.playlist]); 39 | 40 | const setPlaylistName = (name: string): void => 41 | setPlaylist({ ...playlist, title: name }); 42 | 43 | const submit = async (): Promise => { 44 | setLoading(true); 45 | 46 | if (playlist.playlistId) { 47 | return updatePlaylist(playlist, closeDialog); 48 | } 49 | 50 | return createPlaylist(playlist, closeDialog); 51 | }; 52 | 53 | const closeDialog = (): void => { 54 | setLoading(false); 55 | toggleDialog(null); 56 | setTimeout(() => setPlaylist(playlistProps), 600); 57 | }; 58 | 59 | return ( 60 | 61 | 62 | 63 | {t( 64 | playlist?.playlistId 65 | ? 'dialog.createPlaylist.titleUpdate' 66 | : 'dialog.createPlaylist.titleAdd' 67 | )} 68 | 69 | 70 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | export default DialogAddPlaylist; 93 | -------------------------------------------------------------------------------- /src/components/Dialog/EditApiInstance/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Button, TextInput, useTheme, Text } from 'react-native-paper'; 3 | import { Picker } from '@react-native-community/picker'; 4 | import { ApiRoutes } from '../../../constants'; 5 | import { View, Alert } from 'react-native'; 6 | import { actions } from '../../../store'; 7 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 8 | import callApi from '../../../utils/callApi'; 9 | import useInvidiousInstances from '../../../hooks/useInvidiousInstances'; 10 | import { useTranslation } from 'react-i18next'; 11 | import useStore from '../../../hooks/useStore'; 12 | import stripTrailingSlash from '../../../utils/stripTrailingSlash'; 13 | 14 | interface Props { 15 | value: string; 16 | visible: boolean; 17 | onDismiss: () => void; 18 | toggleDialog: () => void; 19 | } 20 | 21 | const DialogEditApiInstance: React.FC = ({ 22 | value, 23 | visible, 24 | onDismiss, 25 | toggleDialog 26 | }) => { 27 | const store = useStore(); 28 | const [instance, setInstance] = useState(value); 29 | const [isLoading, setIsLoading] = useState(false); 30 | const [customInstance, setCustomInstance] = useState(false); 31 | const { instances, loading } = useInvidiousInstances(); 32 | const { t } = useTranslation(); 33 | const { colors } = useTheme(); 34 | 35 | const onValueChange = (value: string): void => { 36 | if (value === 'other') { 37 | return setCustomInstance(true); 38 | } 39 | 40 | return setInstance(stripTrailingSlash(value)); 41 | }; 42 | 43 | const submit = async () => { 44 | setIsLoading(true); 45 | 46 | if (instance === value) { 47 | toggleDialog(); 48 | return setIsLoading(false); 49 | } 50 | 51 | try { 52 | await actions.setInstance(instance); 53 | 54 | if (!store.logoutMode) { 55 | await callApi({ 56 | url: ApiRoutes.Preferences 57 | }); 58 | actions.clearData(); 59 | await fetchPlaylists(); 60 | } 61 | 62 | return setTimeout( 63 | () => 64 | actions.setSnackbar({ 65 | message: t('snackbar.invidiousInstanceUpdated') 66 | }), 67 | 500 68 | ); 69 | } catch (error) { 70 | console.log(error); 71 | 72 | if (!store.logoutMode) { 73 | actions.clearData(); 74 | } 75 | 76 | return setTimeout( 77 | () => 78 | actions.setSnackbar({ 79 | message: t('snackbar.invidiousInstanceTokenUpdated') 80 | }), 81 | 500 82 | ); 83 | } finally { 84 | toggleDialog(); 85 | setIsLoading(false); 86 | } 87 | }; 88 | 89 | return ( 90 | 91 | {t('dialog.editApiInstance.title')} 92 | 93 | {loading ? ( 94 | Loading instances... 95 | ) : ( 96 | 100 | {instances.map(({ uri, monitor }) => ( 101 | 102 | ))} 103 | 104 | )} 105 | 106 | {customInstance && ( 107 | 114 | )} 115 | 116 | 117 | 118 | 119 | 122 | 123 | 124 | ); 125 | }; 126 | 127 | export default DialogEditApiInstance; 128 | -------------------------------------------------------------------------------- /src/components/Dialog/EditToken/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Text, Dialog, Button, TextInput } from 'react-native-paper'; 3 | import { Alert } from 'react-native'; 4 | import callApi from '../../../utils/callApi'; 5 | import { ApiRoutes } from '../../../constants'; 6 | import AsyncStorage from '@react-native-community/async-storage'; 7 | import { actions } from '../../../store'; 8 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 9 | import { useTranslation } from 'react-i18next'; 10 | import Spacer from '../../Spacer'; 11 | 12 | interface Props { 13 | label: string; 14 | value: string; 15 | visible: boolean; 16 | onDismiss: () => void; 17 | toggleDialog: () => void; 18 | } 19 | 20 | const DialogEditToken: React.FC = ({ 21 | label, 22 | value, 23 | visible, 24 | onDismiss, 25 | toggleDialog 26 | }) => { 27 | const { t } = useTranslation(); 28 | const [loading, setLoading] = useState(false); 29 | const [token, setToken] = useState(value ?? ''); 30 | 31 | const onSubmit = async () => { 32 | setLoading(true); 33 | 34 | if (token === value) { 35 | return toggleDialog(); 36 | } 37 | 38 | if (token === '') { 39 | actions.setToken(token); 40 | return toggleDialog(); 41 | } 42 | 43 | try { 44 | await callApi({ 45 | url: ApiRoutes.Preferences, 46 | customToken: token 47 | }); 48 | await fetchPlaylists(); 49 | toggleDialog(); 50 | return setTimeout( 51 | () => 52 | actions.setSnackbar({ 53 | message: t('snackbar.importData') 54 | }), 55 | 500 56 | ); 57 | } catch (error) { 58 | actions.setSnackbar({ 59 | message: error.message 60 | }); 61 | } finally { 62 | setLoading(false); 63 | } 64 | }; 65 | 66 | return ( 67 | 68 | {t('dialog.editToken.title')} 69 | 70 | 77 | {token === '' && ( 78 | <> 79 | 80 | {t('dialog.editToken.emptyToken')} 81 | 82 | )} 83 | 84 | 85 | 86 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | export default DialogEditToken; 95 | -------------------------------------------------------------------------------- /src/components/Dialog/EditUsername/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Button, TextInput } from 'react-native-paper'; 3 | import { Alert } from 'react-native'; 4 | import callApi from '../../../utils/callApi'; 5 | import { ApiRoutes } from '../../../constants'; 6 | import AsyncStorage from '@react-native-community/async-storage'; 7 | import { actions } from '../../../store'; 8 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | interface Props { 12 | label: string; 13 | value: string; 14 | visible: boolean; 15 | onDismiss: () => void; 16 | toggleDialog: () => void; 17 | } 18 | 19 | const DialogEditUsername: React.FC = ({ 20 | label, 21 | value, 22 | visible, 23 | onDismiss, 24 | toggleDialog 25 | }) => { 26 | const [loading, setLoading] = useState(false); 27 | const [username, setUsername] = useState(value); 28 | const { t } = useTranslation(); 29 | 30 | const onSubmit = () => { 31 | setLoading(true); 32 | 33 | try { 34 | actions.setUsername(username); 35 | toggleDialog(); 36 | return setTimeout( 37 | () => 38 | actions.setSnackbar({ 39 | message: t('snackbar.usernameUpdated') 40 | }), 41 | 500 42 | ); 43 | } catch (error) { 44 | actions.setSnackbar({ 45 | message: error.message 46 | }); 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | 52 | return ( 53 | 54 | {t('dialog.editUsername.title')} 55 | 56 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | export default DialogEditUsername; 75 | -------------------------------------------------------------------------------- /src/components/Dialog/ErrorMonitoring/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Button, TextInput, Text, Checkbox } from 'react-native-paper'; 3 | import { View, TouchableNativeFeedback } from 'react-native'; 4 | import callApi from '../../../utils/callApi'; 5 | import { ApiRoutes } from '../../../constants'; 6 | import AsyncStorage from '@react-native-community/async-storage'; 7 | import { actions } from '../../../store'; 8 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 9 | import Spacer from '../../Spacer'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | interface Props { 13 | label: string; 14 | value: string; 15 | visible: boolean; 16 | onDismiss: () => void; 17 | toggleDialog: () => void; 18 | } 19 | 20 | const DialogErrorMonitoring: React.FC = ({ 21 | label, 22 | value, 23 | visible, 24 | onDismiss, 25 | toggleDialog 26 | }) => { 27 | const [loading, setLoading] = useState(false); 28 | const [checked, setChecked] = React.useState(value); 29 | const { t } = useTranslation(); 30 | 31 | const onSubmit = () => { 32 | setLoading(true); 33 | 34 | try { 35 | actions.setSendErrorMonitoring(checked); 36 | toggleDialog(); 37 | return setTimeout( 38 | () => 39 | actions.setSnackbar({ 40 | message: t('snackbar.monitoringSettingsUpdated') 41 | }), 42 | 500 43 | ); 44 | } catch (error) { 45 | actions.setSnackbar({ 46 | message: error.message 47 | }); 48 | } finally { 49 | setLoading(false); 50 | } 51 | }; 52 | 53 | return ( 54 | 55 | {t('dialog.monitoring.title')} 56 | 57 | {t('dialog.monitoring.text1')} 58 | 59 | {t('dialog.monitoring.text2')} 60 | 61 | setChecked(!checked)}> 62 | 72 | 73 | {t('dialog.monitoring.label')} 74 | 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | ); 85 | }; 86 | 87 | export default DialogErrorMonitoring; 88 | -------------------------------------------------------------------------------- /src/components/Dialog/Language/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Button, TextInput, useTheme } from 'react-native-paper'; 3 | import { Picker } from '@react-native-community/picker'; 4 | import { ApiRoutes } from '../../../constants'; 5 | import { View } from 'react-native'; 6 | import { actions } from '../../../store'; 7 | import fetchPlaylists from '../../../utils/fetchPlaylists'; 8 | import callApi from '../../../utils/callApi'; 9 | import { useTranslation } from 'react-i18next'; 10 | import getLanguageName from '../../../utils/getLanguageName'; 11 | 12 | interface Props { 13 | value: string; 14 | visible: boolean; 15 | onDismiss: () => void; 16 | toggleDialog: () => void; 17 | } 18 | 19 | const DialogLanguage: React.FC = ({ 20 | value, 21 | visible, 22 | onDismiss, 23 | toggleDialog 24 | }) => { 25 | const [language, setLanguage] = useState<'en' | 'fr' | 'cs'>(value); 26 | const [isLoading, setIsLoading] = useState(false); 27 | const { t, i18n } = useTranslation(); 28 | const { colors } = useTheme(); 29 | 30 | const submit = async () => { 31 | setIsLoading(true); 32 | 33 | try { 34 | i18n.changeLanguage(language); 35 | await actions.setLanguage(language); 36 | toggleDialog(); 37 | 38 | return setTimeout( 39 | () => 40 | actions.setSnackbar({ 41 | message: t('snackbar.updateLanguage') 42 | }), 43 | 500 44 | ); 45 | } catch (error) { 46 | return setTimeout( 47 | () => 48 | actions.setSnackbar({ 49 | message: error.message 50 | }), 51 | 500 52 | ); 53 | } finally { 54 | toggleDialog(); 55 | setIsLoading(false); 56 | } 57 | }; 58 | 59 | return ( 60 | 61 | {t('dialog.editLanguage.title')} 62 | 63 | 67 | {['en', 'fr', 'cs'].map(lng => ( 68 | 69 | ))} 70 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | export default DialogLanguage; 83 | -------------------------------------------------------------------------------- /src/components/Dialog/RemovePlaylist/index.tsx: -------------------------------------------------------------------------------- 1 | // 2 | import React from 'react'; 3 | import { Dialog, Button, Portal } from 'react-native-paper'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | interface Props { 7 | toggleDialog: () => void; 8 | onPress: () => void; 9 | visible: boolean; 10 | playlistName: string; 11 | loading: boolean; 12 | } 13 | 14 | const DialogRemovePlaylist: React.FC = ({ 15 | toggleDialog, 16 | visible, 17 | onPress, 18 | playlistName, 19 | loading 20 | }) => { 21 | const { t } = useTranslation(); 22 | 23 | return ( 24 | 25 | 26 | 27 | {t('dialog.removePlaylist.title', { playlistName })} 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default DialogRemovePlaylist; 41 | -------------------------------------------------------------------------------- /src/components/Dot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableNativeFeedback, View } from 'react-native'; 3 | import hex2rgba from '../../utils/hex2rgba'; 4 | 5 | interface Props { 6 | isActive: boolean; 7 | color: string; 8 | onPress: () => void; 9 | } 10 | 11 | const Dot: React.FC = ({ isActive, color = '#FFFFFF', onPress }) => ( 12 | 13 | 14 | 22 | 23 | 24 | ); 25 | 26 | export default Dot; 27 | -------------------------------------------------------------------------------- /src/components/Drawler/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, TouchableNativeFeedback } from 'react-native'; 3 | import { 4 | Drawer, 5 | Switch, 6 | Paragraph, 7 | useTheme, 8 | IconButton 9 | } from 'react-native-paper'; 10 | import { actions } from '../../store'; 11 | import AppVersion from '../Version'; 12 | import { useTranslation } from 'react-i18next'; 13 | 14 | interface Props { 15 | darkMode: boolean; 16 | } 17 | 18 | const Drawler: React.FC = ({ 19 | setTheme, 20 | darkMode, 21 | navigation, 22 | drawler 23 | }) => { 24 | const [isDarkMode, setDarkMode] = useState(darkMode); 25 | const { colors } = useTheme(); 26 | const { t } = useTranslation(); 27 | 28 | const toggleDarkMode = (value): void => { 29 | setDarkMode(!isDarkMode); 30 | setTheme(value); 31 | }; 32 | 33 | return ( 34 | 41 | 42 | { 47 | drawler.current.closeDrawer(); 48 | setTimeout(() => navigation.current.navigate('Settings'), 200); 49 | }} 50 | /> 51 | { 56 | drawler.current.closeDrawer(); 57 | setTimeout( 58 | () => navigation.current.navigate('InvidiousInstances'), 59 | 200 60 | ); 61 | }} 62 | /> 63 | 64 | 65 | 70 | 71 | 76 | 81 | 82 | 83 | { 88 | drawler.current.closeDrawer(); 89 | setTimeout(() => navigation.current.navigate('PrivacyPolicy'), 200); 90 | }} 91 | /> 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | const styles = StyleSheet.create({ 99 | container: { 100 | flex: 1 101 | }, 102 | switchContainer: { flexDirection: 'row', paddingRight: 16 } 103 | }); 104 | 105 | export default Drawler; 106 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native-paper'; 3 | 4 | class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | componentDidCatch(): void { 11 | this.setState({ hasError: true }); 12 | } 13 | 14 | render() { 15 | if (this.state.hasError) { 16 | return Oops, something went wrong!; 17 | } 18 | 19 | return this.props.children; 20 | } 21 | } 22 | 23 | export default ErrorBoundary; 24 | -------------------------------------------------------------------------------- /src/components/Favoris/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { IconButton, Button, useTheme } from 'react-native-paper'; 3 | import { actions } from '../../../store'; 4 | import callApi from '../../../utils/callApi'; 5 | import { ApiRoutes } from '../../../constants'; 6 | import { Video, Playlist } from '../../../types'; 7 | import useFavoris from '../../../hooks/useFavoris'; 8 | import { Alert, CheckBox } from 'react-native'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | interface Props { 12 | favorisPlaylist: Playlist; 13 | favorisIds: string[]; 14 | videoId: string; 15 | buttonWithIcon: boolean; 16 | size?: number; 17 | color?: string; 18 | } 19 | 20 | const Favoris: React.FC = ({ 21 | favorisPlaylist, 22 | favorisIds, 23 | video, 24 | buttonWithIcon, 25 | size = 25, 26 | color 27 | }) => { 28 | const { colors, dark } = useTheme(); 29 | const { addToFavoris, removeFromFavoris } = useFavoris(); 30 | const { t } = useTranslation(); 31 | 32 | useEffect(() => {}, []); 33 | 34 | const addOrRemoveToFavoris = (): void => { 35 | if (isFavoris) { 36 | const videoFinded: Video = favorisPlaylist.videos.find( 37 | v => v.videoId === video.videoId 38 | ); 39 | 40 | if (videoFinded.indexId) { 41 | return removeFromFavoris(favorisPlaylist.playlistId, videoFinded); 42 | } else { 43 | return null; 44 | } 45 | } 46 | 47 | return addToFavoris(favorisPlaylist.playlistId, video); 48 | }; 49 | 50 | const isFavoris: boolean = favorisIds.includes(video.videoId); 51 | 52 | const iconColor = { 53 | icon: isFavoris ? 'heart' : 'heart-outline', 54 | color: isFavoris ? colors.favoris : dark ? colors.primary : color 55 | }; 56 | 57 | if (buttonWithIcon) { 58 | return ( 59 | 62 | ); 63 | } 64 | 65 | return ( 66 | 73 | ); 74 | }; 75 | 76 | Favoris.defaultProps = { 77 | buttonWithIcon: false 78 | }; 79 | 80 | export default Favoris; 81 | -------------------------------------------------------------------------------- /src/components/Favoris/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, memo } from 'react'; 2 | import CardList from '../../Card/List'; 3 | import { Playlist, Video } from '../../../types'; 4 | import CardSearch from '../../Card/Search'; 5 | import DialogAddVideoToPlaylist from '../../Dialog/AddVideoToPlaylist'; 6 | import { FAVORIS_PLAYLIST_TITLE } from '../../../constants'; 7 | import DataEmpty from '../../Data/Empty'; 8 | import { Text, Button } from 'react-native-paper'; 9 | import Spacer from '../../Spacer'; 10 | import useFavoris from '../../../hooks/useFavoris'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | interface Props { 14 | videos: Video[]; 15 | playlists?: null | Playlist[]; 16 | favorisIds?: string[]; 17 | isFavoris: boolean; 18 | setPlaylistFrom: string; 19 | } 20 | 21 | const ResultList: React.FC = ({ videos, ...props }) => { 22 | const { createFavorisPlaylist } = useFavoris(); 23 | const { t } = useTranslation(); 24 | 25 | if (!videos) { 26 | return ( 27 | 28 | {t('data.empty.favorisNotSet')} 29 | 30 | 36 | 37 | ); 38 | } 39 | 40 | if (videos.length === 0) { 41 | return ; 42 | } 43 | 44 | return ( 45 | 46 | {videos.map((video, index) => ( 47 | 54 | ))} 55 | 56 | ); 57 | }; 58 | 59 | export default memo(ResultList); 60 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | import { Title } from 'react-native-paper'; 4 | 5 | interface Props { 6 | title: string; 7 | backgroundColor: string; 8 | } 9 | 10 | const Header: React.FC = ({ title, backgroundColor, children }) => ( 11 | 12 | {title} 13 | {children && children} 14 | 15 | ); 16 | 17 | const styles = StyleSheet.create({ 18 | header: { 19 | flexDirection: 'row', 20 | alignItems: 'center', 21 | justifyContent: 'space-between', 22 | marginHorizontal: -16, 23 | paddingHorizontal: 16, 24 | paddingTop: 20, 25 | paddingBottom: 80, 26 | marginBottom: -60 27 | }, 28 | title: { 29 | fontSize: 27, 30 | color: 'white' 31 | } 32 | }); 33 | 34 | export default Header; 35 | -------------------------------------------------------------------------------- /src/components/Instance/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Alert, View } from 'react-native'; 3 | import { Button, Text, Divider, IconButton } from 'react-native-paper'; 4 | import stripTrailingSlash from '../../utils/stripTrailingSlash'; 5 | import { useTranslation } from 'react-i18next'; 6 | import Spacer from '../Spacer'; 7 | import { actions } from '../../store'; 8 | 9 | interface Props { 10 | uri: string; 11 | isCustom: boolean; 12 | setInstance: (value: string) => void; 13 | instance: string; 14 | } 15 | 16 | const Instance: React.FC = ({ 17 | uri, 18 | isCustom, 19 | setInstance, 20 | instance 21 | }) => { 22 | const [isLoading, setIsLoading] = useState(false); 23 | const { t } = useTranslation(); 24 | 25 | const onPress = () => { 26 | setIsLoading(true); 27 | return setInstance(uri, () => setIsLoading(false)); 28 | }; 29 | 30 | const onRemovePress = () => { 31 | actions.removeCustomInstance(uri); 32 | 33 | return setTimeout( 34 | () => 35 | actions.setSnackbar({ 36 | message: t('snackbar.removeCustomInstanceSuccess') 37 | }), 38 | 500 39 | ); 40 | }; 41 | 42 | return ( 43 | <> 44 | 45 | 53 | 54 | {stripTrailingSlash(uri)} 55 | 56 | {instance === stripTrailingSlash(uri) ? ( 57 | null} 62 | animated 63 | style={{ height: 25, width: 50, marginRight: -15 }} 64 | /> 65 | ) : ( 66 | 69 | )} 70 | {isCustom && ( 71 | <> 72 | 73 | 80 | 81 | )} 82 | 83 | 84 | ); 85 | }; 86 | 87 | export default Instance; 88 | -------------------------------------------------------------------------------- /src/components/InstanceList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Alert, StyleSheet, View } from 'react-native'; 3 | import { Button, Text, Divider, IconButton } from 'react-native-paper'; 4 | import stripTrailingSlash from '../../utils/stripTrailingSlash'; 5 | import { useTranslation } from 'react-i18next'; 6 | import Spacer from '../Spacer'; 7 | import { actions } from '../../store'; 8 | import useInvidiousInstances from '../../hooks/useInvidiousInstances'; 9 | import InstanceContainer from '../../containers/Instance'; 10 | import { CustomInstance } from '../../types'; 11 | 12 | interface Props { 13 | customInstances: CustomInstance[]; 14 | } 15 | 16 | const InstanceList: React.FC = ({ customInstances }) => { 17 | const { t } = useTranslation(); 18 | const { 19 | instances, 20 | loading, 21 | setInvidiousInstance, 22 | submitLoading 23 | } = useInvidiousInstances(); 24 | 25 | return ( 26 | 27 | {loading ? ( 28 | 33 | {t('instance.loading')} 34 | 35 | ) : ( 36 | 37 | {[...customInstances, ...instances].map(({ uri, isCustom }) => ( 38 | 44 | ))} 45 | 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | const styles = StyleSheet.create({ 52 | content: { 53 | flexDirection: 'column' 54 | } 55 | }); 56 | 57 | export default InstanceList; 58 | -------------------------------------------------------------------------------- /src/components/Label/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { Text } from 'react-native-paper'; 4 | 5 | interface Props { 6 | align: 'left' | 'right'; 7 | theme: string; 8 | } 9 | 10 | const Label: React.FC = ({ align, theme, children }) => ( 11 | 14 | {children} 15 | 16 | ); 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | position: 'absolute', 21 | bottom: 25, 22 | borderRadius: 4, 23 | color: 'white', 24 | fontSize: 10, 25 | fontWeight: 'bold', 26 | paddingVertical: 2, 27 | paddingHorizontal: 5 28 | }, 29 | left: { 30 | left: 10 31 | }, 32 | right: { 33 | right: 10 34 | } 35 | }); 36 | 37 | export default Label; 38 | -------------------------------------------------------------------------------- /src/components/LastPlays/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, memo, useEffect } from 'react'; 2 | import CardSearch from '../Card/Search'; 3 | import DialogAddVideoToPlaylist from '../Dialog/AddVideoToPlaylist'; 4 | import { actions } from '../../store'; 5 | import Playlist from '../Playlist/List'; 6 | import { SearchVideo, Video, Playlist as PlaylistType } from '../../types'; 7 | import { Text, Title } from 'react-native-paper'; 8 | import Spacer from '../Spacer'; 9 | import { View, Dimensions } from 'react-native'; 10 | import CardScrollList from '../Card/ScrollList'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | interface Props { 14 | setPlaylistFrom: string; 15 | videos: Video[]; 16 | title: string; 17 | } 18 | 19 | const LastPlays: React.FC = ({ setPlaylistFrom, videos, title }) => { 20 | const [dialogIsShow, toggleDialog] = useState(false); 21 | const [video, setVideo] = useState(null); 22 | const { t } = useTranslation(); 23 | 24 | if (!videos || videos.length === 0) { 25 | return null; 26 | } 27 | 28 | return ( 29 | <> 30 | {t('search.lastPlays')} 31 | 32 | {videos.map((video, index) => ( 33 | { 39 | setVideo(item); 40 | toggleDialog(!dialogIsShow); 41 | }} 42 | containerCustomStyle={{ 43 | width: 250, 44 | paddingTop: 15 45 | }} 46 | pictureCustomStyle={{ 47 | height: 130 48 | }} 49 | /> 50 | ))} 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default memo(LastPlays); 57 | -------------------------------------------------------------------------------- /src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView, View, StyleSheet } from 'react-native'; 3 | import { useTheme } from 'react-native-paper'; 4 | import LayoutSpacerContainer from '../../containers/LayoutSpacer'; 5 | 6 | const Layout: React.FC = ({ setTheme, children }) => { 7 | const { colors } = useTheme(); 8 | 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | justifyContent: 'center', 23 | paddingHorizontal: 16 24 | } 25 | }); 26 | 27 | export default Layout; 28 | -------------------------------------------------------------------------------- /src/components/LayoutSpacer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Video } from '../../types'; 3 | import useKeyboard from '../../hooks/useKeyboard'; 4 | import Spacer from '../Spacer'; 5 | 6 | interface Props { 7 | video: null | Video; 8 | } 9 | 10 | const LayoutSpacer: React.FC = ({ video }) => { 11 | const [visible] = useKeyboard(); 12 | 13 | const height = visible || video === null ? 0 : 50; 14 | 15 | return ; 16 | }; 17 | 18 | export default LayoutSpacer; 19 | -------------------------------------------------------------------------------- /src/components/Overlay/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Animated, View, Dimensions, StyleSheet } from 'react-native'; 3 | 4 | const Overlay: React.FC = ({ opacity }) => ( 5 | 14 | ); 15 | 16 | const styles = StyleSheet.create({ 17 | overlay: { 18 | position: 'absolute', 19 | top: 0, 20 | left: 0, 21 | right: 0, 22 | backgroundColor: 'rgba(0, 0, 0, .8)', 23 | height: Dimensions.get('window').height 24 | } 25 | }); 26 | 27 | export default Overlay; 28 | -------------------------------------------------------------------------------- /src/components/Placeholder/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { Placeholder, PlaceholderLine, Fade } from 'rn-placeholder'; 4 | import { stylesVertical } from '../../Card/Layout'; 5 | 6 | const PlaceholderCardSearchItem: React.FC = ({ containerCustomStyle = {} }) => ( 7 | 8 | 9 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | 37 | export default PlaceholderCardSearchItem; 38 | -------------------------------------------------------------------------------- /src/components/Placeholder/CardCenter/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardList from '../../Card/List'; 3 | import PlaceholderCardSearchItem from '../Card'; 4 | import CardScrollList from '../../Card/ScrollList'; 5 | 6 | const PlaceholderCardHorizontalList: React.FC = () => ( 7 | 8 | 14 | 20 | 26 | 32 | 38 | 44 | 45 | ); 46 | 47 | export default PlaceholderCardHorizontalList; 48 | -------------------------------------------------------------------------------- /src/components/Placeholder/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardList from '../../Card/List'; 3 | import PlaceholderCardSearchItem from '../Card'; 4 | 5 | const PlaceholderSearchList: React.FC = () => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export default PlaceholderSearchList; 17 | -------------------------------------------------------------------------------- /src/components/PlayerSmall/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { 3 | View, 4 | Image, 5 | StyleSheet, 6 | Dimensions, 7 | TouchableNativeFeedback, 8 | Animated 9 | } from 'react-native'; 10 | import { Text, Headline, IconButton, Button } from 'react-native-paper'; 11 | import TimeFormat from 'hh-mm-ss'; 12 | import { getColorFromURL } from 'rn-dominant-color'; 13 | import LinearGradient from 'react-native-linear-gradient'; 14 | import { useAnimation } from 'react-native-animation-hooks'; 15 | import { actions } from '../../store'; 16 | import Spacer from '../Spacer'; 17 | import { Video as VideoType } from '../../types'; 18 | import Progress from '../Progress'; 19 | import hex2rgba from '../../utils/hex2rgba'; 20 | import useKeyboard from '../../hooks/useKeyboard'; 21 | import useFavoris from '../../hooks/useFavoris'; 22 | import FavorisButtonContainer from '../../containers/Favoris/Button'; 23 | 24 | interface Props { 25 | video: VideoType; 26 | paused: boolean; 27 | showPlayer: () => void; 28 | } 29 | 30 | const PLAYER_SMALL_HEIGHT = 60; 31 | const WHITE_COLOR = '#FFFFFF'; 32 | 33 | const PlayerSmall: React.FC = ({ video, paused, showPlayer }) => { 34 | const [visible] = useKeyboard(); 35 | const [color, setColor] = useState(WHITE_COLOR); 36 | const [background, setBackground] = useState(WHITE_COLOR); 37 | const bottom = useAnimation({ 38 | toValue: background !== WHITE_COLOR && !visible ? PLAYER_SMALL_HEIGHT : 0, 39 | type: 'timing', 40 | useNativeDriver: false 41 | }); 42 | const { addToFavoris, removeFromFavoris } = useFavoris(); 43 | 44 | getColorFromURL(video?.thumbnail.url).then((colors) => 45 | setBackground(colors.primary) 46 | ); 47 | 48 | useEffect(() => {}, [video]); 49 | 50 | if (!video) { 51 | return null; 52 | } 53 | 54 | return ( 55 | 62 | 63 | 71 | 80 | 81 | 82 | {video?.title} 83 | 84 | 87 | {video?.author} 88 | 89 | 90 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | const styles = StyleSheet.create({ 106 | absoluteContainer: { 107 | position: 'absolute', 108 | overflow: 'hidden', 109 | right: 0, 110 | bottom: 54, 111 | left: 0 112 | }, 113 | container: { 114 | flexDirection: 'row', 115 | alignItems: 'center' 116 | }, 117 | content: { 118 | flex: 1, 119 | marginTop: -2 120 | }, 121 | icon: { 122 | width: 40, 123 | margin: 0, 124 | marginTop: -2, 125 | marginHorizontal: 15 126 | }, 127 | author: { 128 | fontSize: 10 129 | } 130 | }); 131 | 132 | export default PlayerSmall; 133 | -------------------------------------------------------------------------------- /src/components/Playlist/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import CardPlaylist from '../../Card/Playlist'; 4 | import Spacer from '../../Spacer'; 5 | import { setCardItem } from '../../Carousel'; 6 | import DataEmpty from '../../Data/Empty'; 7 | import { Playlist as PlaylistType } from '../../../types'; 8 | import { useTranslation } from 'react-i18next'; 9 | 10 | interface Props { 11 | playlists: PlaylistType[]; 12 | playingVideoId: string; 13 | toggleModal: () => void; 14 | } 15 | 16 | const Playlist: React.FC = ({ 17 | playlists, 18 | playingVideoId, 19 | toggleModal, 20 | logoutMode 21 | }) => { 22 | const { t } = useTranslation(); 23 | 24 | if (playlists.length === 0) { 25 | return ; 26 | } 27 | 28 | return ( 29 | 30 | 31 | {playlists.map(playlist => ( 32 | 40 | ))} 41 | 42 | ); 43 | }; 44 | 45 | export default Playlist; 46 | -------------------------------------------------------------------------------- /src/components/Playlist/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { IconButton, Menu } from 'react-native-paper'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface Props { 6 | onEdit: () => void; 7 | onRemove: () => void; 8 | } 9 | 10 | const PlaylistMenu: React.FC = ({ onEdit, onRemove }) => { 11 | const [menuIsOpen, setToggleMenu] = useState(false); 12 | const { t } = useTranslation(); 13 | 14 | const toggleMenu = (): void => setToggleMenu(!menuIsOpen); 15 | 16 | return ( 17 | 27 | }> 28 | { 30 | onEdit(); 31 | toggleMenu(); 32 | }} 33 | icon="pencil" 34 | title={t('menu.edit')} 35 | /> 36 | { 38 | onRemove(); 39 | toggleMenu(); 40 | }} 41 | icon="delete" 42 | title={t('menu.delete')} 43 | /> 44 | 45 | ); 46 | }; 47 | 48 | export default PlaylistMenu; 49 | -------------------------------------------------------------------------------- /src/components/Profil/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Image, StyleSheet, TouchableOpacity } from 'react-native'; 3 | import { Title, Text, IconButton } from 'react-native-paper'; 4 | import Spacer from '../Spacer'; 5 | import { useTranslation } from 'react-i18next'; 6 | import { useNavigation } from '@react-navigation/native'; 7 | 8 | const Profil: React.FC = ({ username }) => { 9 | const { t } = useTranslation(); 10 | const navigation = useNavigation(); 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | {t('profil.hey', { userName: username })} 18 | 19 | 20 | 21 | navigation.navigate('InvidiousInstances')} 26 | /> 27 | navigation.navigate('Settings')} 32 | style={{ marginHorizontal: 0 }} 33 | /> 34 | 35 | 36 | 37 | {t('profil.welcom')} 38 | 39 | 40 | ); 41 | }; 42 | 43 | const styles = StyleSheet.create({ 44 | container: { 45 | flexDirection: 'row', 46 | alignItems: 'center' 47 | }, 48 | textContainer: { 49 | flex: 1 50 | }, 51 | title: { 52 | color: 'white', 53 | fontSize: 35, 54 | paddingTop: 5 55 | }, 56 | text: { 57 | color: 'white' 58 | } 59 | }); 60 | 61 | export default Profil; 62 | -------------------------------------------------------------------------------- /src/components/Search/Bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { View, StyleSheet, Animated } from 'react-native'; 3 | import { 4 | Searchbar, 5 | Text, 6 | TouchableRipple, 7 | IconButton, 8 | useTheme 9 | } from 'react-native-paper'; 10 | import { useAnimation } from 'react-native-animation-hooks'; 11 | import { actions } from '../../../store'; 12 | import SearchSubmenu from '../Submenu'; 13 | import { useTranslation } from 'react-i18next'; 14 | import { useNavigation } from '@react-navigation/native'; 15 | 16 | const SEARCH_INPUT_PLACEHOLDER = 'Search music'; 17 | 18 | type History = string; 19 | 20 | interface SearchProps { 21 | history: string[]; 22 | showButtonHistory: boolean; 23 | submenuPosition: 'top' | 'bottom'; 24 | } 25 | 26 | const Search: React.FC = ({ 27 | searchValue, 28 | history, 29 | showButtonHistory = false, 30 | submenuPosition = 'top' 31 | }) => { 32 | const [value, setValue] = useState(searchValue); 33 | const [showSubmenu, setShowSubmenu] = useState(false); 34 | const { t } = useTranslation(); 35 | const navigation = useNavigation(); 36 | const { dark, colors } = useTheme(); 37 | 38 | const searchThroughApi = async ( 39 | selectedValue?: null | string = null 40 | ): void => { 41 | if (typeof selectedValue !== 'string' && value === '') { 42 | return actions.setSnackbar({ 43 | message: t('search.emptyValue') 44 | }); 45 | } 46 | 47 | const wantedValue = 48 | typeof selectedValue === 'string' ? selectedValue : value; 49 | await actions.search(wantedValue); 50 | setTimeout(() => navigation.navigate(t('navigation.search')), 200); 51 | }; 52 | 53 | const toggleSubmenu = (): void => setShowSubmenu(!showSubmenu); 54 | 55 | const isBottomPosition = submenuPosition === 'bottom'; 56 | 57 | useEffect(() => { 58 | if (searchValue) { 59 | setValue(searchValue); 60 | } 61 | }, [history]); 62 | 63 | return ( 64 | 69 | 70 | 78 | 79 | {showButtonHistory && history.length > 0 && ( 80 | <> 81 | 89 | 110 | 111 | { 115 | toggleSubmenu(); 116 | searchThroughApi(selectedValue); 117 | }} 118 | isOpen={showSubmenu} 119 | /> 120 | 121 | )} 122 | 123 | ); 124 | }; 125 | 126 | const styles = StyleSheet.create({ 127 | container: { 128 | flexDirection: 'row' 129 | } 130 | }); 131 | 132 | export default Search; 133 | -------------------------------------------------------------------------------- /src/components/Search/BarAbsolute/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Animated } from 'react-native'; 3 | import SearchbarContainer from '../../../containers/Search/Bar'; 4 | import { useAnimation } from 'react-native-animation-hooks'; 5 | import useKeyboard from '../../../hooks/useKeyboard'; 6 | 7 | const SearchbarAbsolute: React.FC = ({ video }) => { 8 | const [visible] = useKeyboard(); 9 | const bottom = useAnimation({ 10 | toValue: visible || video === null ? 15 : 75, 11 | type: 'timing', 12 | delay: 0, 13 | useNativeDriver: false 14 | }); 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | const styles = StyleSheet.create({ 24 | searchBarContainer: { 25 | position: 'absolute', 26 | left: 15, 27 | right: 15 28 | } 29 | }); 30 | 31 | export default SearchbarAbsolute; 32 | -------------------------------------------------------------------------------- /src/components/Search/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Title, Button } from 'react-native-paper'; 3 | import Spacer from '../../Spacer'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { useNavigation } from '@react-navigation/native'; 6 | 7 | interface Props { 8 | value: string; 9 | } 10 | 11 | const SearchEmpty: React.FC = ({ value }) => { 12 | const { t } = useTranslation(); 13 | const { navigate } = useNavigation(); 14 | 15 | return ( 16 | <> 17 | 18 | 19 | {t('search.empty')} {value}. 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default SearchEmpty; 26 | -------------------------------------------------------------------------------- /src/components/Search/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Title, Button } from 'react-native-paper'; 3 | import Spacer from '../../Spacer'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { useNavigation } from '@react-navigation/native'; 6 | 7 | const SearchError: React.FC = () => { 8 | const { t } = useTranslation(); 9 | const { navigate } = useNavigation(); 10 | 11 | return ( 12 | <> 13 | 14 | {t('search.error')} 15 | 16 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default SearchError; 25 | -------------------------------------------------------------------------------- /src/components/Search/PickerType/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Picker } from '@react-native-community/picker'; 3 | import { View } from 'react-native'; 4 | import { actions } from '../../../store'; 5 | import { SearchTypeTypes } from '../../../store/Search'; 6 | import { Text, useTheme } from 'react-native-paper'; 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | const SEARCH_TYPES = ['video', 'playlist']; 10 | 11 | interface Props { 12 | searchType: SearchTypeTypes; 13 | } 14 | 15 | const SearchPickerType = ({ searchType }: Props) => { 16 | const { colors } = useTheme(); 17 | const { t } = useTranslation(); 18 | 19 | return ( 20 | 21 | 25 | {SEARCH_TYPES.map((value, index) => ( 26 | 31 | ))} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default SearchPickerType; 38 | -------------------------------------------------------------------------------- /src/components/Search/Popular/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, memo, useEffect } from 'react'; 2 | import { useQuery } from 'react-query'; 3 | import CardSearch from '../../Card/Search'; 4 | import DialogAddVideoToPlaylist from '../../Dialog/AddVideoToPlaylist'; 5 | import { actions } from '../../../store'; 6 | import Playlist from '../../Playlist/List'; 7 | import { SearchVideo, Video, Playlist as PlaylistType } from '../../../types'; 8 | import { Text, Title, Button, IconButton } from 'react-native-paper'; 9 | import Spacer from '../../Spacer'; 10 | import { View, Dimensions, StyleSheet } from 'react-native'; 11 | import CardScrollList from '../../Card/ScrollList'; 12 | import { useTranslation } from 'react-i18next'; 13 | import { useNavigation } from '@react-navigation/native'; 14 | import search from '../../../queries/search'; 15 | import PlaceholderCardHorizontalList from '../../Placeholder/CardCenter'; 16 | import SearchError from '../Error'; 17 | 18 | interface Props { 19 | playlists: PlaylistType[]; 20 | setPlaylistFrom: string; 21 | apiUrl: string; 22 | title: string; 23 | instance: string; 24 | } 25 | 26 | const SearchPopularTop: React.FC = ({ 27 | title, 28 | setPlaylistFrom, 29 | apiUrl, 30 | instance 31 | }) => { 32 | const [enabled, setRefetch] = useState(true); 33 | const { isLoading, error, data } = useQuery(apiUrl, search, { 34 | enabled, 35 | onSuccess: () => setRefetch(false) 36 | }); 37 | const { t } = useTranslation(); 38 | const { navigate } = useNavigation(); 39 | 40 | useEffect(() => { 41 | if (data) { 42 | actions.receiveData({ key: setPlaylistFrom, data }); 43 | } 44 | }, [data, instance]); 45 | 46 | const Header = () => ( 47 | 48 | {title} 49 | setRefetch(true)} /> 50 | 51 | ); 52 | 53 | if (isLoading) { 54 | return ; 55 | } 56 | 57 | if (error || !Array.isArray(data) || data.length === 0) { 58 | return ( 59 | <> 60 |
61 | 62 | 63 | ); 64 | } 65 | 66 | return ( 67 | <> 68 |
69 | 70 | {data.map((video, index) => ( 71 | 84 | ))} 85 | 86 | 87 | ); 88 | }; 89 | 90 | const styles = StyleSheet.create({ 91 | header: { 92 | flexDirection: 'row', 93 | justifyContent: 'space-between', 94 | alignItems: 'center' 95 | } 96 | }); 97 | 98 | export default memo(SearchPopularTop); 99 | -------------------------------------------------------------------------------- /src/components/Search/Result/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, memo, useEffect } from 'react'; 2 | import { useQuery } from 'react-query'; 3 | import CardList from '../../Card/List'; 4 | import CardSearch from '../../Card/Search'; 5 | import DialogAddVideoToPlaylist from '../../Dialog/AddVideoToPlaylist'; 6 | import { actions } from '../../../store'; 7 | import Playlist from '../../Playlist/List'; 8 | import { SearchVideo, Video, Playlist as PlaylistType } from '../../../types'; 9 | import { Text, useTheme, Button } from 'react-native-paper'; 10 | import Spacer from '../../Spacer'; 11 | import DataEmpty from '../../Data/Empty'; 12 | import { useTranslation } from 'react-i18next'; 13 | import { useNavigation } from '@react-navigation/native'; 14 | import search from '../../../queries/search'; 15 | import SearchError from '../Error'; 16 | import PlaceholderSearchList from '../../Placeholder/Search'; 17 | import SearchEmpty from '../Empty'; 18 | 19 | interface Props { 20 | apiUrl: string; 21 | searchValue: string; 22 | } 23 | 24 | const SearchResult: React.FC = ({ apiUrl, searchValue }) => { 25 | const { isLoading, error, data } = useQuery(apiUrl, search); 26 | const { colors } = useTheme(); 27 | const { t } = useTranslation(); 28 | const { navigate } = useNavigation(); 29 | 30 | useEffect(() => { 31 | if (data) { 32 | actions.setSearchResult(data); 33 | } 34 | }, [data]); 35 | 36 | if (isLoading) { 37 | return ; 38 | } 39 | 40 | if (error || !Array.isArray(data)) { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | if (data.length === 0) { 49 | return ( 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | return ( 57 | 58 | {data.map((video, index) => ( 59 | 66 | ))} 67 | 68 | ); 69 | }; 70 | 71 | export default memo(SearchResult); 72 | -------------------------------------------------------------------------------- /src/components/Search/Submenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { View, StyleSheet, Animated } from 'react-native'; 3 | import { 4 | Searchbar, 5 | Text, 6 | TouchableRipple, 7 | IconButton, 8 | useTheme 9 | } from 'react-native-paper'; 10 | import { useAnimation } from 'react-native-animation-hooks'; 11 | import { actions } from '../../../store'; 12 | 13 | interface Props { 14 | isOpen: boolean; 15 | selectValue: () => void; 16 | items: string[]; 17 | position?: 'top' | 'bottom'; 18 | } 19 | 20 | const SearchSubmenu: React.FC = ({ 21 | isOpen, 22 | selectValue, 23 | items, 24 | position = 'top' 25 | }) => { 26 | const { colors, dark } = useTheme(); 27 | const opacity = useAnimation({ 28 | toValue: isOpen ? 1 : 0, 29 | type: 'timing', 30 | duration: 100, 31 | useNativeDriver: false 32 | }); 33 | 34 | const isBottomPosition = position === 'bottom'; 35 | 36 | return ( 37 | 47 | {items.map((text, index) => ( 48 | selectValue(text)}> 49 | 59 | {text} 60 | 61 | 62 | ))} 63 | 64 | ); 65 | }; 66 | 67 | const styles = StyleSheet.create({ 68 | submenu: { 69 | position: 'absolute', 70 | left: 0, 71 | right: 0, 72 | elevation: 2, 73 | zIndex: 2, 74 | borderRadius: 4 75 | }, 76 | item: { 77 | padding: 7 78 | } 79 | }); 80 | 81 | export default SearchSubmenu; 82 | -------------------------------------------------------------------------------- /src/components/Search/Value/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { Text } from 'react-native-paper'; 4 | 5 | const SearchValue: React.FC = ({ value, resultCount }) => { 6 | if ((value === null) | (value === '')) { 7 | return null; 8 | } 9 | 10 | return ( 11 | 12 | {value} ({resultCount}) 13 | 14 | ); 15 | }; 16 | 17 | const styles = StyleSheet.create({ 18 | text: { 19 | color: 'white' 20 | } 21 | }); 22 | 23 | export default SearchValue; 24 | -------------------------------------------------------------------------------- /src/components/Snackbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimensions } from 'react-native'; 3 | import { Snackbar as PaperSnackBar } from 'react-native-paper'; 4 | import { actions } from '../../store'; 5 | import { Snackbar as SnackbarType } from '../../types'; 6 | 7 | interface Props { 8 | snackbar: SnackbarType; 9 | } 10 | 11 | const SNACKBAR_DURATION: number = 5000; 12 | 13 | const Snackbar: React.FC = ({ snackbar }) => { 14 | return ( 15 | actions.hideSnackbar()} 20 | action={snackbar.action}> 21 | {snackbar.message} 22 | 23 | ); 24 | }; 25 | 26 | export default Snackbar; 27 | -------------------------------------------------------------------------------- /src/components/Spacer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | interface Props { 5 | width?: number; 6 | height?: number; 7 | } 8 | 9 | const Spacer: React.FC = ({ width, height }) => ( 10 | 11 | ); 12 | 13 | Spacer.defaultProps = { 14 | width: 0, 15 | height: 0 16 | }; 17 | 18 | export default Spacer; 19 | -------------------------------------------------------------------------------- /src/components/Version/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Button, List, Text, TouchableRipple } from 'react-native-paper'; 4 | import { version } from '../../../package'; 5 | import useUpdateRelease from '../../hooks/useUpdateRelease'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | interface Props { 9 | customStyle?: { 10 | [key: string]: string | number; 11 | }; 12 | showUpdate: boolean; 13 | } 14 | 15 | const AppVersion: React.FC = ({ customStyle, showUpdate = true }) => { 16 | const { t } = useTranslation(); 17 | const { updateAvailable, downloadApk } = useUpdateRelease(); 18 | 19 | return ( 20 | 21 | 22 | 27 | 28 | {updateAvailable && showUpdate && ( 29 | 34 | 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | 46 | const styles = StyleSheet.create({ 47 | text: { marginTop: 'auto', padding: 10, fontSize: 11 } 48 | }); 49 | 50 | export default AppVersion; 51 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const ApiRoutes = { 2 | VideoId: (videoId: string) => `videos/${videoId}`, 3 | Preferences: 'auth/preferences', 4 | Playlists: 'auth/playlists', 5 | PlaylistId: (playlistId: string) => `playlists/${playlistId}`, 6 | Videos: (playlistId: string) => `auth/playlists/${playlistId}/videos`, 7 | VideoIndexId: (playlistId: string, indexId: string) => 8 | `playlists/${playlistId}/videos/${indexId}` 9 | }; 10 | 11 | export const FAVORIS_PLAYLIST_TITLE: string = 'favoris'; 12 | -------------------------------------------------------------------------------- /src/containers/CarouselSpacer/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Spacer from '../../components/Spacer'; 3 | 4 | const CarouselSpacerContainer = connect(({ playlists }: Store) => ({ 5 | height: playlists.length <= 1 ? 30 : 90 6 | }))(Spacer); 7 | 8 | export default CarouselSpacerContainer; 9 | -------------------------------------------------------------------------------- /src/containers/DialogAddVideoToPlaylist/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store, actions } from '../../store'; 2 | import DialogAddVideoToPlaylist from '../../components/Dialog/AddVideoToPlaylist'; 3 | import { Playlist } from '../../types'; 4 | import { FAVORIS_PLAYLIST_TITLE } from '../../constants'; 5 | 6 | const DialogAddVideoToPlaylistContainer = connect( 7 | ({ playlists, dialogAddVideoToPlaylist }: Store) => ({ 8 | visible: dialogAddVideoToPlaylist.isOpen, 9 | video: dialogAddVideoToPlaylist.video, 10 | toggleDialog: actions.toggleDialogAddVideoToPlaylist, 11 | playlists: playlists.filter( 12 | (p: Playlist) => p.title !== FAVORIS_PLAYLIST_TITLE 13 | ) 14 | }) 15 | )(DialogAddVideoToPlaylist); 16 | 17 | export default DialogAddVideoToPlaylistContainer; 18 | -------------------------------------------------------------------------------- /src/containers/Drawler/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Drawler from '../../components/Drawler'; 3 | 4 | const DrawlerContainer = connect(({ darkMode }: Store) => ({ 5 | darkMode 6 | }))(Drawler); 7 | 8 | export default DrawlerContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Favoris/Button/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import FavorisButton from '../../../components/Favoris/Button'; 3 | 4 | const FavorisButtonContainer = connect(({ favorisPlaylist }: Store) => ({ 5 | favorisPlaylist, 6 | favorisIds: favorisPlaylist 7 | ? favorisPlaylist.videos.map((v) => v.videoId) 8 | : [] 9 | }))(FavorisButton); 10 | 11 | export default FavorisButtonContainer; 12 | -------------------------------------------------------------------------------- /src/containers/Favoris/Playlist/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import FavorisPlaylist from '../../../components/Favoris/List'; 3 | 4 | const FavorisPlaylistContainer = connect(({ favorisPlaylist }: Store) => ({ 5 | videos: favorisPlaylist?.videos ?? null, 6 | favorisIds: favorisPlaylist?.videos.map((v) => v.videoId) ?? [], 7 | setPlaylistFrom: 'favoris', 8 | isFavoris: true 9 | }))(FavorisPlaylist); 10 | 11 | export default FavorisPlaylistContainer; 12 | -------------------------------------------------------------------------------- /src/containers/Instance/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Instance from '../../components/Instance'; 3 | 4 | const InstanceContainer = connect(({ instance }: Store) => ({ 5 | instance 6 | }))(Instance); 7 | 8 | export default InstanceContainer; 9 | -------------------------------------------------------------------------------- /src/containers/InstanceList/index.ts: -------------------------------------------------------------------------------- 1 | import InstanceList from '../../components/InstanceList'; 2 | import { connect, Store } from '../../store'; 3 | 4 | const InstanceListContainer = connect(({ customInstances }: Store) => ({ 5 | customInstances 6 | }))(InstanceList); 7 | 8 | export default InstanceListContainer; 9 | -------------------------------------------------------------------------------- /src/containers/LastPlays/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import LastPlays from '../../components/LastPlays'; 3 | 4 | const LastPlaysContainer = connect(({ lastPlays }: Store) => ({ 5 | videos: lastPlays 6 | }))(LastPlays); 7 | 8 | export default LastPlaysContainer; 9 | -------------------------------------------------------------------------------- /src/containers/LayoutSpacer/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import LayoutSpacer from '../../components/LayoutSpacer'; 3 | 4 | const LayoutSpacerContainer = connect(({ video }: Store) => ({ 5 | video 6 | }))(LayoutSpacer); 7 | 8 | export default LayoutSpacerContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Player/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Player from '../../components/Player'; 3 | 4 | const PlayerContainer = connect( 5 | ({ video, videoIndex, paused, repeat, playerIsOpened, playlist }: Store) => { 6 | const nextVideoIndex = 7 | playlist && playlist.length > 1 ? videoIndex + 1 : null; 8 | const previousVideoIndex = 9 | playlist && playlist.length > 1 ? videoIndex - 1 : null; 10 | 11 | return { 12 | video, 13 | nextVideoIndex, 14 | previousVideoIndex, 15 | paused, 16 | repeat, 17 | playerIsOpened, 18 | isLastVideo: nextVideoIndex === (playlist && playlist.length), 19 | isFirstVideo: videoIndex === 0 20 | }; 21 | } 22 | )(Player); 23 | 24 | export default PlayerContainer; 25 | -------------------------------------------------------------------------------- /src/containers/PlayerSmall/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import PlayerSmall from '../../components/PlayerSmall'; 3 | 4 | const PlayerSmallContainer = connect(({ video, paused }: Store) => ({ 5 | video, 6 | paused 7 | }))(PlayerSmall); 8 | 9 | export default PlayerSmallContainer; 10 | -------------------------------------------------------------------------------- /src/containers/Playlist/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store, actions } from '../../store'; 2 | import Video from '../../components/Video'; 3 | 4 | const PlaylistContainer = connect(({ playlist, video, paused }: Store) => ({ 5 | videos: playlist, 6 | showRemoveButton: false, 7 | playingVideoId: video?.videoId, 8 | paused, 9 | onPlay: async (videoIndex: number) => actions.loadVideo({ videoIndex }) 10 | }))(Video); 11 | 12 | export default PlaylistContainer; 13 | -------------------------------------------------------------------------------- /src/containers/Playlists/Carousel/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import Carousel from '../../../components/Carousel'; 3 | 4 | const PlaylistsCarouselContainer = connect(({ playlists }: Store) => ({ 5 | playlists: playlists.filter((p) => p.title !== 'favoris').slice(0, 5) 6 | }))(Carousel); 7 | 8 | export default PlaylistsCarouselContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Playlists/List/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import Playlist from '../../../components/Playlist/List'; 3 | 4 | const PlaylistsContainer = connect( 5 | ({ playlists, video, logoutMode }: Store) => ({ 6 | playlists: playlists.filter(p => p.title !== 'favoris'), 7 | playingVideoId: video?.videoId, 8 | logoutMode 9 | }) 10 | )(Playlist); 11 | 12 | export default PlaylistsContainer; 13 | -------------------------------------------------------------------------------- /src/containers/Profil/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Profil from '../../components/Profil'; 3 | 4 | const ProfilContainer = connect(({ username }: Store) => ({ 5 | username 6 | }))(Profil); 7 | 8 | export default ProfilContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Search/Bar/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import Searchbar from '../../../components/Search/Bar'; 3 | 4 | const SearchbarContainer = connect(({ history, searchValue }: Store) => ({ 5 | history, 6 | searchValue 7 | }))(Searchbar); 8 | 9 | export default SearchbarContainer; 10 | -------------------------------------------------------------------------------- /src/containers/Search/BarAbsolute/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import SearchbarAbsolute from '../../../components/Search/BarAbsolute'; 3 | 4 | const SearchbarAbsoluteContainer = connect(({ video }: Store) => ({ 5 | video 6 | }))(SearchbarAbsolute); 7 | 8 | export default SearchbarAbsoluteContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Search/PickerType/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import SearchPickerType from '../../../components/Search/PickerType'; 3 | 4 | const SearchPickerTypeContainer = connect(({ searchType }: Store) => ({ 5 | searchType 6 | }))(SearchPickerType); 7 | 8 | export default SearchPickerTypeContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Search/Popular/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import Popular from '../../../components/Search/Popular'; 3 | 4 | const SearchPopularContainer = connect(({ instance }: Store) => ({ 5 | instance 6 | }))(Popular); 7 | 8 | export default SearchPopularContainer; 9 | -------------------------------------------------------------------------------- /src/containers/Search/Result/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import SearchResult from '../../../components/Search/Result'; 3 | import { FAVORIS_PLAYLIST_TITLE } from '../../../constants'; 4 | 5 | const SearchResultContainer = connect( 6 | ({ 7 | searchValue, 8 | searchType, 9 | }: Store) => ({ 10 | apiUrl: searchValue === '' ? `popular` : `search?q=${searchValue}&type=${searchType}`, 11 | searchValue 12 | }) 13 | )(SearchResult); 14 | 15 | export default SearchResultContainer; 16 | -------------------------------------------------------------------------------- /src/containers/Search/Value/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../../store'; 2 | import SearchValue from '../../../components/Search/Value'; 3 | 4 | const SearchValueContainer = connect(({ searchValue, results }: Store) => ({ 5 | value: searchValue, 6 | resultCount: results.length 7 | }))(SearchValue); 8 | 9 | export default SearchValueContainer; 10 | -------------------------------------------------------------------------------- /src/containers/Snackbar/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, Store } from '../../store'; 2 | import Snackbar from '../../components/Snackbar'; 3 | 4 | const SnackbarContainer = connect(({ snackbar }: Store) => ({ 5 | snackbar 6 | }))(Snackbar); 7 | 8 | export default SnackbarContainer; 9 | -------------------------------------------------------------------------------- /src/hooks/useBackup.ts: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs'; 2 | import useStore from './useStore'; 3 | import { actions } from '../store'; 4 | import { PermissionsAndroid, Alert } from 'react-native'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | const fileName = 'holoplay-backup.json'; 8 | const path = `${RNFS.DownloadDirectoryPath}/${fileName}`; 9 | 10 | const useBackup = () => { 11 | const store = useStore(); 12 | const { t } = useTranslation(); 13 | 14 | const backupData = async (): Promise => { 15 | await requestWriteExternalStoragePermission(); 16 | 17 | if (await RNFS.exists(path)) { 18 | await RNFS.unlink(path); 19 | } 20 | 21 | RNFS.writeFile( 22 | path, 23 | JSON.stringify({ 24 | playlists: store.playlists, 25 | favorisPlaylist: store.favorisPlaylist 26 | }), 27 | 'utf8' 28 | ) 29 | .then(() => 30 | actions.setSnackbar({ 31 | message: t('snackbar.dataExportSuccess') 32 | }) 33 | ) 34 | .catch(() => { 35 | actions.setSnackbar({ message: t('snackbar.dataExportError') }); 36 | }); 37 | }; 38 | 39 | const importData = async (): Promise => { 40 | await requestWriteExternalStoragePermission(); 41 | 42 | RNFS.readDir(RNFS.DownloadDirectoryPath) 43 | .then(files => { 44 | const backupFile = files.find(file => file.name === fileName); 45 | return RNFS.readFile(backupFile.path, 'utf8'); 46 | }) 47 | .then(async data => { 48 | await actions.importData(JSON.parse(data)); 49 | actions.setSnackbar({ 50 | message: t('snackbar.dataImportSuccess') 51 | }); 52 | }) 53 | .catch(() => { 54 | actions.setSnackbar({ 55 | message: t('snackbar.dataImportError') 56 | }); 57 | }); 58 | }; 59 | 60 | return { 61 | backupData, 62 | importData 63 | }; 64 | }; 65 | 66 | export const requestWriteExternalStoragePermission = async () => { 67 | try { 68 | const test = await PermissionsAndroid.request( 69 | PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE 70 | ); 71 | 72 | if (test === PermissionsAndroid.RESULTS.GRANTED) { 73 | return Promise.resolve(PermissionsAndroid.RESULTS.GRANTED); 74 | } 75 | 76 | return Promise.reject('Reject'); 77 | } catch (error) { 78 | console.logo(error); 79 | } 80 | }; 81 | 82 | export default useBackup; 83 | -------------------------------------------------------------------------------- /src/hooks/useDownloadFile.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import downloadFile from '../utils/downloadFile'; 3 | import { DownloadFileParams } from '../utils/downloadFile'; 4 | import { actions } from '../store'; 5 | 6 | interface UseDownloadFileHook { 7 | loading: boolean; 8 | downloadVideo: (params: DownloadFileParams) => Promise; 9 | } 10 | 11 | const useDownloadFile = (): UseDownloadFileHook => { 12 | const [loading, setLoading] = useState(false); 13 | 14 | const downloadVideo = async (params: DownloadFileParams): Promise => { 15 | try { 16 | setLoading(true); 17 | await downloadFile(params); 18 | actions.setSnackbar({ 19 | message: `${params.fileName} has been donwload.` 20 | }); 21 | } catch (error) { 22 | console.log(error); 23 | } finally { 24 | setLoading(false); 25 | } 26 | }; 27 | 28 | return { 29 | loading, 30 | downloadVideo 31 | }; 32 | }; 33 | 34 | export default useDownloadFile; 35 | -------------------------------------------------------------------------------- /src/hooks/useFavoris.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuiv4 } from 'uuid'; 2 | import callApi from '../utils/callApi'; 3 | import { FAVORIS_PLAYLIST_TITLE, ApiRoutes } from '../constants'; 4 | import { actions } from '../store'; 5 | import useStore from './useStore'; 6 | import { Playlist } from '../types'; 7 | import Video from 'react-native-video'; 8 | import { useTranslation } from 'react-i18next'; 9 | 10 | const useFavoris = () => { 11 | const store = useStore(); 12 | const { t } = useTranslation(); 13 | 14 | const createFavorisPlaylist = async () => { 15 | const favorisPlaylist = { 16 | title: FAVORIS_PLAYLIST_TITLE, 17 | privacy: 'public' 18 | }; 19 | 20 | if (!store.logoutMode) { 21 | try { 22 | await callApi({ 23 | url: ApiRoutes.Playlists, 24 | method: 'POST', 25 | body: favorisPlaylist 26 | }); 27 | 28 | return setTimeout( 29 | () => 30 | actions.setSnackbar({ 31 | message: t('snackbar.playlistFavorisCreateSuccess') 32 | }), 33 | 500 34 | ); 35 | } catch (error) { 36 | return setTimeout( 37 | () => actions.setSnackbar({ message: error.message }), 38 | 500 39 | ); 40 | } 41 | } 42 | 43 | return actions.addPlaylist({ 44 | ...favorisPlaylist, 45 | playlistId: uuiv4(), 46 | videos: [] 47 | }); 48 | }; 49 | 50 | const addToFavoris = async (playlistId: Playlist, video: Video) => { 51 | try { 52 | if (!store.logoutMode) { 53 | await callApi({ 54 | url: ApiRoutes.Videos(playlistId), 55 | method: 'POST', 56 | body: { 57 | videoId: video.videoId 58 | } 59 | }); 60 | 61 | actions.addToFavoris(video); 62 | } else { 63 | actions.addToFavoris({ 64 | ...video, 65 | indexId: uuiv4() 66 | }); 67 | } 68 | 69 | return actions.setSnackbar({ 70 | message: t('snackbar.addFavorisSuccess') 71 | }); 72 | } catch (error) { 73 | return actions.setSnackbar({ 74 | message: error.message 75 | }); 76 | } 77 | }; 78 | 79 | const removeFromFavoris = async (playlistId: string, video: Video) => { 80 | try { 81 | actions.removeFromFavoris(video.videoId); 82 | 83 | if (!store.logoutMode) { 84 | await callApi({ 85 | url: ApiRoutes.VideoIndexId(playlistId, video.indexId), 86 | method: 'DELETE' 87 | }); 88 | } 89 | 90 | return actions.setSnackbar({ 91 | message: t('snackbar.removeFavorisSuccess') 92 | }); 93 | } catch (error) { 94 | return actions.setSnackbar({ 95 | message: error.message 96 | }); 97 | } 98 | }; 99 | 100 | return { 101 | createFavorisPlaylist, 102 | addToFavoris, 103 | removeFromFavoris 104 | }; 105 | }; 106 | 107 | export default useFavoris; 108 | -------------------------------------------------------------------------------- /src/hooks/useInvidiousInstances.ts: -------------------------------------------------------------------------------- 1 | import { Instance } from '../types/Api'; 2 | import { useState, useEffect } from 'react'; 3 | import fetchInvidiousInstances from '../utils/fetchInvidiousInstances'; 4 | import { actions } from '../store'; 5 | import callApi from '../utils/callApi'; 6 | import fetchPlaylists from '../utils/fetchPlaylists'; 7 | import { useTranslation } from 'react-i18next'; 8 | import stripTrailingSlash from '../utils/stripTrailingSlash'; 9 | import useStore from './useStore'; 10 | 11 | interface UseInvidiousInstancesHook { 12 | instances: Instance[]; 13 | setInvidiousInstance: (instance: string, callback?: any) => Promise; 14 | loading: boolean; 15 | } 16 | 17 | const useInvidiousInstances = (): UseInvidiousInstancesHook => { 18 | const [instances, setInstances] = useState([]); 19 | const [loading, setLoading] = useState(true); 20 | const [submitLoading, setSubmitLoading] = useState(false); 21 | const { t } = useTranslation(); 22 | const store = useStore(); 23 | 24 | const setInvidiousInstance = async ( 25 | instance: string, 26 | callback?: () => any 27 | ): Promise => { 28 | setSubmitLoading(true); 29 | 30 | try { 31 | await actions.setInstance(stripTrailingSlash(instance)); 32 | 33 | if (!store.logoutMode) { 34 | await callApi({ 35 | url: ApiRoutes.Preferences 36 | }); 37 | actions.clearData(); 38 | await fetchPlaylists(); 39 | } 40 | 41 | return setTimeout( 42 | () => 43 | actions.setSnackbar({ 44 | message: t('snackbar.invidiousInstanceUpdated') 45 | }), 46 | 500 47 | ); 48 | } catch (error) { 49 | console.log(error); 50 | 51 | if (!store.logoutMode) { 52 | actions.clearData(); 53 | } 54 | 55 | return setTimeout( 56 | () => 57 | actions.setSnackbar({ 58 | message: t('snackbar.invidiousInstanceTokenUpdated') 59 | }), 60 | 500 61 | ); 62 | } finally { 63 | if (callback) { 64 | callback(); 65 | } 66 | setSubmitLoading(false); 67 | } 68 | }; 69 | 70 | useEffect(() => { 71 | fetchInvidiousInstances().then(instances => { 72 | setInstances(instances); 73 | setLoading(false); 74 | }); 75 | }, []); 76 | 77 | return { 78 | instances, 79 | setInvidiousInstance, 80 | loading 81 | }; 82 | }; 83 | 84 | export default useInvidiousInstances; 85 | -------------------------------------------------------------------------------- /src/hooks/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Keyboard } from 'react-native'; 3 | 4 | interface UseKeyboardHook { 5 | visible: boolean; 6 | dismiss: () => void; 7 | } 8 | 9 | const useKeyboard = (config = {}): UseKeyboardHook => { 10 | const { useWillShow = false, useWillHide = false } = config; 11 | const [visible, setVisible] = useState(false); 12 | const showEvent = useWillShow ? 'keyboardWillShow' : 'keyboardDidShow'; 13 | const hideEvent = useWillHide ? 'keyboardWillHide' : 'keyboardDidHide'; 14 | 15 | function dismiss() { 16 | Keyboard.dismiss(); 17 | setVisible(false); 18 | } 19 | 20 | useEffect(() => { 21 | function onKeyboardShow() { 22 | setVisible(true); 23 | } 24 | 25 | function onKeyboardHide() { 26 | setVisible(false); 27 | } 28 | 29 | Keyboard.addListener(showEvent, onKeyboardShow); 30 | Keyboard.addListener(hideEvent, onKeyboardHide); 31 | 32 | return () => { 33 | Keyboard.removeListener(showEvent, onKeyboardShow); 34 | Keyboard.removeListener(hideEvent, onKeyboardHide); 35 | }; 36 | }, [useWillShow, useWillHide]); 37 | 38 | return [visible, dismiss]; 39 | }; 40 | 41 | export default useKeyboard; 42 | -------------------------------------------------------------------------------- /src/hooks/useLinking.ts: -------------------------------------------------------------------------------- 1 | import { Linking } from 'react-native'; 2 | import { useCallback } from 'react'; 3 | import { actions } from '../store'; 4 | 5 | interface UseLinkingHook { 6 | openUrl: (url: string) => Promise; 7 | } 8 | 9 | const useLinking = (): UseLinkingHook => { 10 | const openUrl = async (url: string) => { 11 | const supported = await Linking.canOpenURL(url); 12 | 13 | if (supported) { 14 | await Linking.openURL(url); 15 | } else { 16 | actions.setSnackbar({ 17 | message: `Don't know how to open this URL: ${url}` 18 | }); 19 | } 20 | }; 21 | 22 | return { 23 | openUrl 24 | }; 25 | }; 26 | 27 | export default useLinking; 28 | -------------------------------------------------------------------------------- /src/hooks/usePlaylist.ts: -------------------------------------------------------------------------------- 1 | import 'react-native-get-random-values'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import callApi from '../utils/callApi'; 4 | import { ApiRoutes } from '../constants'; 5 | import { actions } from '../store'; 6 | import { Playlist } from '../types'; 7 | import { useState } from 'react'; 8 | import useStore from './useStore'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | const usePlaylist = (): void => { 12 | const store = useStore(); 13 | const { t } = useTranslation(); 14 | 15 | const createPlaylist = async ( 16 | playlist: Playlist, 17 | callback: () => void, 18 | showSnackbar: boolean = true 19 | ): Promise => { 20 | const playlistName = playlist.title; 21 | 22 | try { 23 | if (!store.logoutMode) { 24 | await callApi({ 25 | url: ApiRoutes.Playlists, 26 | method: 'POST', 27 | body: { 28 | title: playlist.title, 29 | privacy: 'public' 30 | } 31 | }); 32 | } 33 | 34 | actions.addPlaylist({ 35 | playlistId: playlist.playlistId ?? uuidv4(), 36 | title: playlist.title, 37 | privacy: 'public', 38 | videos: [] 39 | }); 40 | } catch (error) { 41 | console.log(error); 42 | actions.setSnackbar({ 43 | message: t('snackbar.removePlaylist', { 44 | playlistName: error.message 45 | }) 46 | }); 47 | } 48 | 49 | if (callback) { 50 | callback(); 51 | } 52 | 53 | if (showSnackbar) { 54 | return setTimeout( 55 | () => 56 | actions.setSnackbar({ 57 | message: t('snackbar.createPlaylist', { playlistName }) 58 | }), 59 | 500 60 | ); 61 | } else { 62 | return null; 63 | } 64 | }; 65 | 66 | const updatePlaylist = async ( 67 | playlist: Playlist, 68 | callback: () => void 69 | ): Promise => { 70 | try { 71 | // Updating store before because this callApi return an error if success ... 72 | actions.updatePlaylist({ 73 | ...playlist, 74 | title: playlist.title 75 | }); 76 | actions.setSnackbar({ 77 | message: t('snackbar.updatePlaylist', { 78 | playlistName: playlist.title 79 | }) 80 | }); 81 | 82 | if (!store.logoutMode) { 83 | await callApi({ 84 | url: ApiRoutes.PlaylistId(playlist.playlistId), 85 | method: 'PATCH', 86 | body: { 87 | title: playlist.title, 88 | privacy: 'public' 89 | } 90 | }); 91 | } 92 | } catch (error) { 93 | console.log(error); 94 | actions.setSnackbar({ 95 | message: t('snackbar.removePlaylist', { 96 | playlistName: error.message 97 | }) 98 | }); 99 | // actions.setSnackbar({message: `Error : ${playlist.title} not updated.`}); 100 | } finally { 101 | if (callback) { 102 | callback(); 103 | } 104 | } 105 | }; 106 | 107 | const removePlaylist = async ( 108 | playlist: Playlist, 109 | callback: () => void 110 | ): Promise => { 111 | try { 112 | // Updating store before because this callApi return an error if success ... 113 | actions.removePlaylist(playlist.playlistId); 114 | actions.setSnackbar({ 115 | message: t('snackbar.removePlaylist', { 116 | playlistName: playlist.title 117 | }) 118 | }); 119 | 120 | if (!store.logoutMode) { 121 | await callApi({ 122 | url: ApiRoutes.PlaylistId(playlist.playlistId), 123 | method: 'DELETE' 124 | }); 125 | } 126 | } catch (error) { 127 | actions.setSnackbar({ 128 | message: t('snackbar.removePlaylist', { 129 | playlistName: error.message 130 | }) 131 | }); 132 | } finally { 133 | if (callback) { 134 | callback(); 135 | } 136 | } 137 | }; 138 | 139 | return { 140 | createPlaylist, 141 | updatePlaylist, 142 | removePlaylist 143 | }; 144 | }; 145 | 146 | export default usePlaylist; 147 | -------------------------------------------------------------------------------- /src/hooks/useStore.ts: -------------------------------------------------------------------------------- 1 | import { getState, Store } from '../store'; 2 | 3 | const useStore = (): Store => { 4 | const store = getState(); 5 | return store; 6 | }; 7 | 8 | export default useStore; 9 | -------------------------------------------------------------------------------- /src/hooks/useUpdateRelease.ts: -------------------------------------------------------------------------------- 1 | import config from 'react-native-config'; 2 | import { useEffect, useState } from 'react'; 3 | import semverCompare from 'semver-compare'; 4 | import RNFetchBlob from 'rn-fetch-blob'; 5 | import { version } from '../../package'; 6 | import fetchHopRelease from '../utils/fetchGithubAppVersion'; 7 | import { actions } from '../store'; 8 | import downloadApk from '../utils/downloadApk'; 9 | 10 | interface UseUpdateReleaseHook { 11 | updateAvailable: boolean; 12 | downloadApk: () => void; 13 | } 14 | 15 | const useUpdateRelease = ( 16 | showSnackbar: boolean = false 17 | ): UseUpdateReleaseHook => { 18 | const [url, setUrl] = useState(null); 19 | const [fileName, setFileName] = useState(null); 20 | const [updateAvailable, setUpdateAvailable] = useState(false); 21 | 22 | useEffect(() => { 23 | if (config.GITHUB_RELEASE === 'true') { 24 | fetchHopRelease().then(({ tagName, browserDownloadUrl }) => { 25 | if (semverCompare(tagName, version) === 1) { 26 | setUrl(browserDownloadUrl); 27 | setFileName(`holoplay-${tagName}.apk`); 28 | setUpdateAvailable(true); 29 | 30 | if (showSnackbar) { 31 | showUpdateIsAvailable(); 32 | } 33 | } 34 | }); 35 | } 36 | }); 37 | 38 | const showUpdateIsAvailable = () => { 39 | setTimeout( 40 | () => 41 | actions.setSnackbar({ 42 | message: 'A new update is available', 43 | action: { 44 | label: 'Download', 45 | onPress: () => downloadApk(url, fileName) 46 | } 47 | }), 48 | 1000 49 | ); 50 | }; 51 | 52 | return { 53 | updateAvailable, 54 | downloadApk: () => downloadApk(url, fileName) 55 | }; 56 | }; 57 | export default useUpdateRelease; 58 | -------------------------------------------------------------------------------- /src/hooks/useVideo.ts: -------------------------------------------------------------------------------- 1 | import { ApiRoutes } from '../constants'; 2 | import callApi from '../utils/callApi'; 3 | import { actions } from '../store'; 4 | import Video from 'react-native-video'; 5 | import useStore from './useStore'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const useVideo = () => { 9 | const store = useStore(); 10 | const { t } = useTranslation(); 11 | 12 | const addVideoToPlaylist = async ( 13 | playlistId: string, 14 | video: Video, 15 | callback: () => void 16 | ): Promise => { 17 | if (!store.logoutMode) { 18 | try { 19 | await callApi({ 20 | url: ApiRoutes.Videos(playlistId), 21 | method: 'POST', 22 | body: { 23 | videoId: video.videoId 24 | } 25 | }); 26 | } catch (error) { 27 | console.log(error); 28 | } 29 | } 30 | 31 | actions.addToPlaylist({ 32 | playlistId, 33 | video 34 | }); 35 | 36 | if (callback) { 37 | callback(); 38 | } 39 | 40 | return actions.setSnackbar({ 41 | message: t('snackbar.addVideoToPlaylistSuccess', { 42 | videoName: video.title 43 | }) 44 | }); 45 | }; 46 | 47 | const removeVideo = async ( 48 | videoIndexId: string, 49 | playlistId: string 50 | ): Promise => { 51 | if (!store.logoutMode) { 52 | try { 53 | await callApi({ 54 | url: ApiRoutes.VideoIndexId(playlistId, videoIndexId), 55 | method: 'DELETE' 56 | }); 57 | } catch (error) { 58 | console.log(error); 59 | } 60 | } 61 | 62 | actions.removeFromPlaylist({ 63 | playlistId: playlistId, 64 | indexId: videoIndexId 65 | }); 66 | 67 | return actions.setSnackbar({ 68 | message: t('snackbar.removeVideoFromPlaylistSuccess', { 69 | videoName: videoIndexId 70 | }) 71 | }); 72 | }; 73 | 74 | return { 75 | addVideoToPlaylist, 76 | removeVideo 77 | }; 78 | }; 79 | 80 | export default useVideo; 81 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import AsyncStorage from '@react-native-community/async-storage'; 4 | 5 | const initI18n = async () => { 6 | const lng = await AsyncStorage.getItem('language'); 7 | 8 | i18n.use(initReactI18next).init({ 9 | resources: { 10 | en: require('./en'), 11 | fr: require('./fr'), 12 | cs: require('./cs') 13 | }, 14 | lng: lng ?? 'en' 15 | }); 16 | }; 17 | 18 | initI18n(); 19 | 20 | export default initI18n; 21 | -------------------------------------------------------------------------------- /src/queries/search.ts: -------------------------------------------------------------------------------- 1 | import { getState, Store } from "../store"; 2 | import { SearchVideo } from "../types"; 3 | 4 | const search = async (url: string): Promise => { 5 | const store: Store = getState(); 6 | const request = await fetch(`${store.instance}/api/v1/${url}`); 7 | const result = await request.json() 8 | 9 | return Array.isArray(result) ? result.slice(0, 20) : []; 10 | } 11 | 12 | export default search; 13 | -------------------------------------------------------------------------------- /src/screens/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | import { Title, useTheme, Text } from 'react-native-paper'; 4 | import Layout from '../../components/Layout'; 5 | import Spacer from '../../components/Spacer'; 6 | import Carousel from '../../components/Carousel'; 7 | import SearchResultContainer from '../../containers/Search/Result'; 8 | import PlaylistsCarouselContainer from '../../containers/Playlists/Carousel'; 9 | import CarouselSpacerContainer from '../../containers/CarouselSpacer'; 10 | import SearchPickerTypeContainer from '../../containers/Search/PickerType'; 11 | import { DASHBOARD_COLOR } from '../../../config/theme'; 12 | import ProfilContainer from '../../containers/Profil'; 13 | import { useTranslation } from 'react-i18next'; 14 | import LastPlaysContainer from '../../containers/LastPlays'; 15 | import PlaceholderCardHorizontalList from '../../components/Placeholder/CardCenter'; 16 | import SearchPopularContainer from '../../containers/Search/Popular'; 17 | import ErrorBoundary from '../../components/ErrorBoundary'; 18 | 19 | const DashboardScreen: React.FC = ({ route }) => { 20 | const { colors } = useTheme(); 21 | const { t } = useTranslation(); 22 | 23 | return ( 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 51 | 52 | ); 53 | }; 54 | 55 | const styles = StyleSheet.create({ 56 | header: { 57 | marginHorizontal: -16, 58 | paddingHorizontal: 16 59 | }, 60 | searchHeader: { 61 | flexDirection: 'row', 62 | alignItems: 'center', 63 | justifyContent: 'space-between' 64 | } 65 | }); 66 | 67 | export default DashboardScreen; 68 | -------------------------------------------------------------------------------- /src/screens/Favoris/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../../components/Layout'; 3 | import Header from '../../components/Header'; 4 | import FavorisPlaylistContainer from '../../containers/Favoris/Playlist'; 5 | import { FAVORIS_COLOR } from '../../../config/theme'; 6 | import { useTheme } from 'react-native-paper'; 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | const Favoris: React.FC = ({ route }) => { 10 | const { colors } = useTheme(); 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | 15 |
19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Favoris; 25 | -------------------------------------------------------------------------------- /src/screens/InvidiousInstances/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { View, StyleSheet, Dimensions, RefreshControl } from 'react-native'; 3 | import { 4 | Appbar, 5 | Subheading, 6 | List, 7 | Divider, 8 | Button, 9 | Text, 10 | Checkbox, 11 | useTheme 12 | } from 'react-native-paper'; 13 | import useStore from '../../hooks/useStore'; 14 | import useBackup from '../../hooks/useBackup'; 15 | import DialogEditToken from '../../components/Dialog/EditToken'; 16 | import DialogEditApiInstance from '../../components/Dialog/EditApiInstance'; 17 | import DialogEditUsername from '../../components/Dialog/EditUsername'; 18 | import DialogErrorMonitoring from '../../components/Dialog/ErrorMonitoring'; 19 | import DialogLanguage from '../../components/Dialog/Language'; 20 | import { useTranslation } from 'react-i18next'; 21 | import getLanguageName from '../../utils/getLanguageName'; 22 | import { ScrollView } from 'react-native-gesture-handler'; 23 | import Spacer from '../../components/Spacer'; 24 | import useInvidiousInstances from '../../hooks/useInvidiousInstances'; 25 | import stripTrailingSlash from '../../utils/stripTrailingSlash'; 26 | import InstanceContainer from '../../containers/Instance'; 27 | import { NavigationHelpersCommon } from '@react-navigation/native'; 28 | import DialogAddCustomInstance from '../../components/Dialog/AddCustomInstance'; 29 | import InstanceListContainer from '../../containers/InstanceList'; 30 | 31 | interface Props { 32 | navigation: NavigationHelpersCommon; 33 | } 34 | 35 | const DEVICE_HEIGHT = Dimensions.get('window').height; 36 | 37 | const wait = (timeout) => { 38 | return new Promise((resolve) => { 39 | setTimeout(resolve, timeout); 40 | }); 41 | }; 42 | 43 | const InvidiousInstanceScreen: React.FC = ({ navigation }) => { 44 | const { t } = useTranslation(); 45 | const { colors } = useTheme(); 46 | const [dialogIsOpen, setDialogIsOpen] = useState(false); 47 | 48 | return ( 49 | <> 50 | 51 | 56 | 57 | navigation.goBack()} 61 | /> 62 | 66 | setDialogIsOpen(true)} /> 67 | 68 | 69 | 70 | 71 | setDialogIsOpen(false)} 74 | /> 75 | 76 | ); 77 | }; 78 | 79 | const styles = StyleSheet.create({ 80 | container: { 81 | flex: 1 82 | } 83 | }); 84 | 85 | export default InvidiousInstanceScreen; 86 | -------------------------------------------------------------------------------- /src/screens/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, View } from 'react-native'; 3 | 4 | const LoadingScreen: React.FC = () => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default LoadingScreen; 11 | -------------------------------------------------------------------------------- /src/screens/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | import AppVersion from '../../components/Version'; 4 | import LoginForm from './form'; 5 | 6 | interface Props { 7 | setToken: () => void; 8 | } 9 | 10 | const LoginScreen: React.FC = ({ route }) => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | justifyContent: 'center', 23 | padding: 16 24 | } 25 | }); 26 | 27 | export default LoginScreen; 28 | -------------------------------------------------------------------------------- /src/screens/Playlists/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Portal, FAB, useTheme } from 'react-native-paper'; 3 | import Layout from '../../components/Layout'; 4 | import DialogAddPlaylist from '../../components/Dialog/AddPlaylist'; 5 | import Header from '../../components/Header'; 6 | import Playlist from '../../components/Playlist/List'; 7 | import PlaylistsContainer from '../../containers/Playlists/List'; 8 | import { Playlist as PlaylistType } from '../../types/Api'; 9 | import { useIsFocused } from '@react-navigation/native'; 10 | import { PLAYLISTS_COLOR } from '../../../config/theme'; 11 | import { StyleSheet } from 'react-native'; 12 | import { useTranslation } from 'react-i18next'; 13 | 14 | const PlaylistScreen: React.FC = ({ route }) => { 15 | const [modalIsOpen, setToggleModal] = useState(false); 16 | const [playlist, setPlaylist] = useState(null); 17 | const [fabIsOpen, toggleFab] = useState(false); 18 | const isFocused = useIsFocused(); 19 | const { colors } = useTheme(); 20 | const { t } = useTranslation(); 21 | 22 | const toggleModal = (item: null | PlaylistType = null): void => { 23 | if (item?.playlistId) { 24 | setPlaylist(item); 25 | } 26 | 27 | setToggleModal(!modalIsOpen); 28 | }; 29 | 30 | return ( 31 | 32 |
33 | { 35 | setPlaylist(item); 36 | toggleModal(); 37 | }} 38 | /> 39 | 44 | 45 | setToggleModal(true) 54 | } 55 | ]} 56 | onStateChange={({ open }): void => toggleFab(open)} 57 | fabStyle={[ 58 | style.fab, 59 | { 60 | backgroundColor: colors.fabGroup 61 | } 62 | ]} 63 | color="white" 64 | /> 65 | 66 | 67 | ); 68 | }; 69 | 70 | const style = StyleSheet.create({ 71 | fab: { 72 | marginBottom: 70 73 | } 74 | }); 75 | 76 | export default PlaylistScreen; 77 | -------------------------------------------------------------------------------- /src/screens/PrivacyPolicy/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, Dimensions, TouchableHighlight, Linking } from 'react-native'; 3 | import { 4 | Appbar, 5 | Subheading, 6 | Divider, 7 | Text, 8 | useTheme 9 | } from 'react-native-paper'; 10 | import { useTranslation } from 'react-i18next'; 11 | import { ScrollView } from 'react-native-gesture-handler'; 12 | import Spacer from '../../components/Spacer'; 13 | import { NavigationHelpersCommon } from '@react-navigation/native'; 14 | 15 | interface Props { 16 | navigation: NavigationHelpersCommon; 17 | } 18 | 19 | const DEVICE_HEIGHT = Dimensions.get('window').height; 20 | 21 | const PrivacyPolicyScreen: React.FC = ({ navigation }) => { 22 | const { t } = useTranslation(); 23 | const { colors } = useTheme(); 24 | 25 | return ( 26 | 27 | 32 | 33 | navigation.goBack()} 37 | /> 38 | 42 | 43 | 44 | 45 | {t('privacyPolicy.intro1')} HoloPlay {t('privacyPolicy.intro2')} Stéphane Richin 46 | 47 | {t('privacyPolicy.dataTitle')} 48 | 49 | {t('privacyPolicy.dataText')} 50 | 51 | 52 | 53 | {t('privacyPolicy.emailTitle')} 54 | 55 | {t('privacyPolicy.emailText1')}, {t('privacyPolicy.emailText2')} 56 | 57 | Linking.openURL('mailto:contact@stephane-richin.fr')}>contact@stephane-richin.fr 58 | 59 | 60 | {t('privacyPolicy.crashReportingTitle')} 61 | {t('privacyPolicy.crashReportingText1')} 62 | 63 | {t('privacyPolicy.crashReportingText2')} 64 | 65 | 66 | {t('privacyPolicy.permissionsTitle')} 67 | {t('privacyPolicy.permissionsText1')} 68 | 69 | {t('privacyPolicy.permissionsText2')} 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | const styles = StyleSheet.create({ 78 | container: { 79 | flex: 1 80 | }, 81 | content: { 82 | flexDirection: 'column', 83 | paddingHorizontal: 20 84 | }, 85 | subheading: { 86 | fontWeight: 'bold', 87 | paddingTop: 16, 88 | paddingBottom: 8 89 | } 90 | }); 91 | 92 | export default PrivacyPolicyScreen; 93 | -------------------------------------------------------------------------------- /src/screens/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { Title, useTheme } from 'react-native-paper'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useIsFocused } from '@react-navigation/native'; 5 | import Layout from '../../components/Layout'; 6 | import SearchResultContainer from '../../containers/Search/Result'; 7 | import Header from '../../components/Header'; 8 | import SearchbarAbsoluteContainer from '../../containers/Search/BarAbsolute'; 9 | import SearchPickerTypeContainer from '../../containers/Search/PickerType'; 10 | 11 | const SearchScreen: React.FC = ({ route }) => { 12 | const { colors } = useTheme(); 13 | const { t } = useTranslation(); 14 | const isFocused = useIsFocused(); 15 | 16 | return ( 17 | <> 18 | 19 |
22 | 23 |
24 | 25 |
26 | 27 | 28 | ); 29 | }; 30 | 31 | export default SearchScreen; 32 | -------------------------------------------------------------------------------- /src/store/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import { Store } from '../../store'; 3 | import { CustomInstance, Video } from '../../types'; 4 | 5 | export interface DialogState { 6 | dialogAddVideoToPlaylist: { 7 | isOpen: boolean; 8 | video: null | Video; 9 | }; 10 | } 11 | 12 | const dialogState: DialogState = { 13 | dialogAddVideoToPlaylist: { 14 | isOpen: false, 15 | video: null 16 | } 17 | }; 18 | 19 | const dialogActions = { 20 | setVideoDialogAddVideoToPlaylist: ( 21 | store: Store, 22 | actions: any, 23 | video: Video 24 | ) => { 25 | return { 26 | ...store, 27 | dialogAddVideoToPlaylist: { 28 | ...store.dialogAddVideoToPlaylist, 29 | video, 30 | isOpen: true 31 | } 32 | }; 33 | }, 34 | toggleDialogAddVideoToPlaylist: (store: Store) => { 35 | return { 36 | ...store, 37 | dialogAddVideoToPlaylist: { 38 | ...store.dialogAddVideoToPlaylist, 39 | isOpen: !store.dialogAddVideoToPlaylist.isOpen 40 | } 41 | }; 42 | } 43 | }; 44 | 45 | export { dialogState, dialogActions }; 46 | -------------------------------------------------------------------------------- /src/store/Player/index.ts: -------------------------------------------------------------------------------- 1 | import callApi from '../../utils/callApi'; 2 | import config from 'react-native-config'; 3 | import { ApiRoutes } from '../../constants'; 4 | import { Video, Playlist, VideoThumbnail } from '../../types'; 5 | import { getState, Store } from '../../store'; 6 | import { Alert } from 'react-native'; 7 | import AsyncStorage from '@react-native-community/async-storage'; 8 | import { getPlaylist, setIsLastPlay } from '../utils'; 9 | 10 | export interface PlayerState { 11 | playerIsOpened: boolean; 12 | video: null | Video; 13 | videoIndex: null | number; 14 | repeat: boolean; 15 | paused: boolean; 16 | duration: number; 17 | playlist: null | Video[]; 18 | lastPlays: Video[]; 19 | } 20 | 21 | const playerState: PlayerState = { 22 | playerIsOpened: false, 23 | video: null, 24 | videoIndex: null, 25 | repeat: false, 26 | paused: false, 27 | duration: 0, 28 | playlist: null, 29 | lastPlays: [] 30 | }; 31 | 32 | const playerActions = { 33 | showPlayer: async (store: Store): Promise => ({ 34 | ...store, 35 | playerIsOpened: store.video !== null 36 | }), 37 | hidePlayer: async (store: Store): Promise => ({ 38 | ...store, 39 | playerIsOpened: false 40 | }), 41 | loadVideo: async ( 42 | store: Store, 43 | actions: any, 44 | { 45 | videoIndex, 46 | setPlaylistFrom 47 | }: { videoIndex: number; setPlaylistFrom: undefined | PlaylistOrigin } 48 | ): Promise => { 49 | const playlist = await getPlaylist(setPlaylistFrom); 50 | const isLastVideo = playlist.length === videoIndex; 51 | // If is last video, we restart the playlist from first index 52 | const video: Video = isLastVideo ? playlist[0] : playlist[videoIndex]; 53 | const data = await callApi({ url: ApiRoutes.VideoId(video.videoId) }); 54 | 55 | const videoUpdated = { 56 | ...video, 57 | ...data, 58 | uri: data.liveNow 59 | ? `${config.YOUTUBE_AUDIO_SERVER_API_URL}/${data.videoId}` 60 | : data.adaptiveFormats.find( 61 | ({ type }: any) => type === 'audio/webm; codecs="opus"' 62 | ).url, 63 | thumbnail: data.videoThumbnails.find( 64 | ({ quality }: VideoThumbnail) => quality === 'medium' 65 | ) 66 | }; 67 | 68 | const lastPlays = setIsLastPlay(video, store.lastPlays); 69 | 70 | return { 71 | ...store, 72 | video: videoUpdated, 73 | videoIndex: videoIndex, 74 | lastPlays, 75 | playlist 76 | }; 77 | }, 78 | loadLiveVideo: async ( 79 | store: Store, 80 | actions: any, 81 | { 82 | videoIndex, 83 | data, 84 | setPlaylistFrom 85 | }: { 86 | videoIndex: number; 87 | data: Video; 88 | setPlaylistFrom: undefined | PlaylistOrigin; 89 | } 90 | ): Promise => { 91 | const playlist = await getPlaylist(setPlaylistFrom); 92 | const isLastVideo = playlist.length === videoIndex; 93 | // If is last video, we restart the playlist from first index 94 | const video: Video = isLastVideo ? playlist[0] : playlist[videoIndex]; 95 | 96 | const videoUpdated = { 97 | ...video, 98 | ...data, 99 | uri: `${config.YOUTUBE_AUDIO_SERVER_API_URL}/${data.videoId}`, 100 | thumbnail: data.videoThumbnails.find( 101 | ({ quality }: VideoThumbnail) => quality === 'medium' 102 | ) 103 | }; 104 | 105 | const lastPlays = setIsLastPlay(video, store.lastPlays); 106 | 107 | return { 108 | ...store, 109 | video: videoUpdated, 110 | videoIndex: videoIndex, 111 | lastPlays, 112 | playlist 113 | }; 114 | }, 115 | paused: async (store: Store): Promise => ({ 116 | ...store, 117 | paused: !store.paused 118 | }), 119 | repeat: async (store: Store): Promise => ({ 120 | ...store, 121 | repeat: !store.repeat 122 | }), 123 | loadPlaylist: async (store: Store, actions: any, playlistId: string) => { 124 | const playlist: Playlist = await callApi({ 125 | url: ApiRoutes.PlaylistId(playlistId) 126 | }); 127 | 128 | return { 129 | ...store, 130 | playlist: playlist.videos 131 | }; 132 | } 133 | }; 134 | 135 | export { playerActions, playerState }; 136 | -------------------------------------------------------------------------------- /src/store/Search/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import { SearchVideo } from '../../types'; 3 | import { Store } from '../../store'; 4 | 5 | export type SearchTypeTypes = 'video' | 'playlist'; 6 | 7 | export interface SearchState { 8 | searchValue: string; 9 | searchType: SearchTypeTypes; 10 | results: SearchVideo[]; 11 | popular: SearchVideo[]; 12 | top: SearchVideo[]; 13 | history: string[]; 14 | } 15 | 16 | const searchState: SearchState = { 17 | searchValue: '', 18 | searchType: 'video', 19 | results: [], 20 | popular: [], 21 | top: [], 22 | history: [] 23 | }; 24 | 25 | const searchActions = { 26 | search: async (store: Store, actions: any, value: string): Promise => { 27 | let history = store.history; 28 | 29 | if (value) { 30 | history = [value, ...history.slice(0, 4)]; 31 | await AsyncStorage.setItem('searchHistory', JSON.stringify(history)); 32 | } 33 | 34 | return { 35 | ...store, 36 | searchValue: value, 37 | history 38 | }; 39 | }, 40 | setSearchResult: async ( 41 | store: Store, 42 | actions: any, 43 | results: SearchVideo[] 44 | ): Promise => ({ 45 | ...store, 46 | results 47 | }), 48 | receiveData: ( 49 | store: Store, 50 | actions: any, 51 | { key, data }: { key: string; data: SearchVideo[] } 52 | ) => ({ 53 | ...store, 54 | [key]: data 55 | }), 56 | setSearchType: ( 57 | store: Store, 58 | actions: any, 59 | searchType: SearchTypeTypes 60 | ): Store => ({ 61 | ...store, 62 | searchType 63 | }), 64 | }; 65 | 66 | export { searchActions, searchState }; 67 | -------------------------------------------------------------------------------- /src/store/Snackbar/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import { Store } from '../../store'; 3 | import { CustomInstance, Snackbar, Video } from '../../types'; 4 | 5 | export interface SnackbarState { 6 | snackbar: Snackbar; 7 | } 8 | 9 | const snackbarState: SnackbarState = { 10 | snackbar: { 11 | message: null, 12 | visible: false, 13 | action: { 14 | label: 'Close', 15 | onPress: (): null => null 16 | } 17 | } 18 | }; 19 | 20 | const snackbarActions = { 21 | setSnackbar: (store: Store, actions: any, snackbar: Snackbar): Store => { 22 | if (snackbar.action) { 23 | setTimeout((): void => actions.setDefaultSnackbarAction(), 7000); 24 | } 25 | 26 | return { 27 | ...store, 28 | snackbar: { 29 | ...store.snackbar, 30 | ...snackbar, 31 | visible: true 32 | } 33 | }; 34 | }, 35 | hideSnackbar: (store: Store): Store => ({ 36 | ...store, 37 | snackbar: { 38 | ...store.snackbar, 39 | visible: false 40 | } 41 | }), 42 | setDefaultSnackbarAction: (store: Store): Store => ({ 43 | ...store, 44 | snackbar: { 45 | ...store.snackbar, 46 | action: snackbarState.snackbar.action 47 | } 48 | }) 49 | }; 50 | 51 | export { snackbarState, snackbarActions }; 52 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import createStore from 'react-waterfall'; 2 | import { searchState, searchActions, SearchState } from './Search'; 3 | import { playerState, playerActions, PlayerState } from './Player'; 4 | import { dataState, dataActions, DataState } from './Data'; 5 | import { appState, appActions, AppState } from './App'; 6 | import { snackbarState, snackbarActions, SnackbarState } from './Snackbar'; 7 | import { dialogActions, dialogState, DialogState } from './Dialog'; 8 | 9 | export interface Store 10 | extends AppState, 11 | DataState, 12 | PlayerState, 13 | SearchState, 14 | SnackbarState, 15 | DialogState {} 16 | 17 | interface ConfigStore { 18 | initialState: Store; 19 | actionsCreators: { 20 | [key: string]: any; 21 | }; 22 | } 23 | 24 | const config: ConfigStore = { 25 | initialState: { 26 | ...dialogState, 27 | ...snackbarState, 28 | ...searchState, 29 | ...playerState, 30 | ...dataState, 31 | ...appState 32 | }, 33 | actionsCreators: { 34 | ...dialogActions, 35 | ...snackbarActions, 36 | ...searchActions, 37 | ...playerActions, 38 | ...dataActions, 39 | ...appActions 40 | } 41 | }; 42 | 43 | export const { Provider, connect, actions, getState } = createStore(config); 44 | -------------------------------------------------------------------------------- /src/store/utils.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import Video from 'react-native-video'; 3 | import { getState } from '.'; 4 | import { FAVORIS_PLAYLIST_TITLE } from '../constants'; 5 | 6 | type PlaylistOrigin = 7 | | 'searchResult' 8 | | 'popular' 9 | | 'trending' 10 | | 'lastPlays' 11 | | 'favoris'; 12 | 13 | export const getPlaylist = async ( 14 | origin: undefined | PlaylistOrigin | Video 15 | ): void => { 16 | const store = await getState(); 17 | 18 | if (origin === undefined) { 19 | return store.playlist; 20 | } 21 | 22 | let playlistList; 23 | 24 | switch (true) { 25 | case origin === 'searchResults': 26 | playlistList = store.results; 27 | break; 28 | case origin === 'popular': 29 | playlistList = store.popular; 30 | break; 31 | case origin === 'trending': 32 | playlistList = store.trending; 33 | break; 34 | case origin === 'lastPlays': 35 | playlistList = store.lastPlays; 36 | break; 37 | case origin === 'favoris': 38 | playlistList = store.playlists.find( 39 | p => p.title === FAVORIS_PLAYLIST_TITLE 40 | )?.videos; 41 | break; 42 | case typeof origin === 'object': 43 | playlistList = origin; 44 | break; 45 | } 46 | 47 | if (origin.videoId) { 48 | playlistList = origin.videos; 49 | } 50 | 51 | return playlistList; 52 | }; 53 | 54 | export const setIsLastPlay = (video: Video, lastPlays: Video[]) => { 55 | const lastPlayVideo = lastPlays[0]; 56 | const isAlreadyLastPlay = 57 | (video?.videoId || video) === (lastPlayVideo?.videoId || lastPlayVideo?.id); 58 | 59 | if (isAlreadyLastPlay) { 60 | return lastPlays; 61 | } 62 | 63 | const lastPlaysUpdated = [video, ...lastPlays.slice(0, 9)]; 64 | AsyncStorage.setItem('lastPlays', JSON.stringify(lastPlaysUpdated)); 65 | 66 | return lastPlaysUpdated; 67 | }; 68 | -------------------------------------------------------------------------------- /src/types/QuickActions/index.ts: -------------------------------------------------------------------------------- 1 | export interface QuickAction { 2 | icon: string; 3 | title: string; 4 | type: string; 5 | userInfo: { url: string }; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/Snackbar/index.ts: -------------------------------------------------------------------------------- 1 | export interface Snackbar { 2 | message: null | string; 3 | visible: boolean; 4 | action: SnackbarAction; 5 | } 6 | 7 | interface SnackbarAction { 8 | label: string; 9 | onPress: () => null | void; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Api'; 2 | export * from './Snackbar'; 3 | export * from './QuickActions'; 4 | -------------------------------------------------------------------------------- /src/utils/ISO8601toDuration.ts: -------------------------------------------------------------------------------- 1 | import formatTimeUnit from './formatTimeUnit'; 2 | 3 | const ISO8601toDuration = (input: string): string => { 4 | const M = formatTimeUnit(input, 'M'); 5 | const S = formatTimeUnit(input, 'S'); 6 | let H = formatTimeUnit(input, 'H'); 7 | 8 | if (H == '00') { 9 | H = ''; 10 | } else { 11 | H += ':'; 12 | } 13 | 14 | return H + M + ':' + S; 15 | }; 16 | 17 | export default ISO8601toDuration; 18 | -------------------------------------------------------------------------------- /src/utils/callApi.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | 3 | interface Args { 4 | url: string; 5 | method?: 'POST' | 'GET' | 'DELETE' | 'PATCH'; 6 | body?: { 7 | [key: string]: string; 8 | }; 9 | customToken: string; 10 | } 11 | 12 | const DEFAULT_HEADERS = { 13 | 'Content-Type': 'application/json' 14 | }; 15 | 16 | const callApi = async ({ 17 | url, 18 | method, 19 | body, 20 | customToken 21 | }: Args): Promise => { 22 | const [instance, token, logoutMode] = await Promise.all([ 23 | AsyncStorage.getItem('instance'), 24 | AsyncStorage.getItem('token'), 25 | AsyncStorage.getItem('logoutMode') 26 | ]); 27 | 28 | const params: any = { 29 | method: method ?? 'GET', 30 | headers: DEFAULT_HEADERS 31 | }; 32 | 33 | if (!JSON.parse(logoutMode) || customToken) { 34 | params.headers = { 35 | ...params.headers, 36 | Authorization: `Bearer ${token || customToken}` 37 | }; 38 | } 39 | 40 | if (body) { 41 | params.body = JSON.stringify(body); 42 | } 43 | 44 | if (__DEV__) { 45 | console.log(`${params.method} - ${instance}/api/v1/${url}`); 46 | } 47 | 48 | const request = await fetch(`${instance}/api/v1/${url}`, params); 49 | const response = await request.json(); 50 | 51 | if (response.error || response.statusCode >= 400 && response.statusCode < 500) { 52 | throw Error(response.error || response); 53 | } 54 | 55 | return response; 56 | }; 57 | 58 | export default callApi; 59 | -------------------------------------------------------------------------------- /src/utils/downloadApk.ts: -------------------------------------------------------------------------------- 1 | import RNFetchBlob from 'rn-fetch-blob'; 2 | import RNFS from 'react-native-fs'; 3 | import { actions } from '../store'; 4 | import { requestWriteExternalStoragePermission } from '../hooks/useBackup'; 5 | 6 | const { android } = RNFetchBlob; 7 | 8 | const downloadApk = async (url: string, fileName: string): Promise => { 9 | actions.setSnackbar({ 10 | message: 'Download have started' 11 | }); 12 | 13 | await requestWriteExternalStoragePermission(); 14 | 15 | return RNFetchBlob.config({ 16 | addAndroidDownloads: { 17 | useDownloadManager: true, 18 | title: fileName, 19 | path: `${RNFS.DownloadDirectoryPath}/hop-release.apk`, 20 | mime: 'application/vnd.android.package-archive', 21 | mediaScannable: true, 22 | notification: true 23 | } 24 | }) 25 | .fetch('GET', url) 26 | .then(res => { 27 | actions.setSnackbar({ 28 | message: `New apk has been download ! Go to your download folder and run apk file` 29 | }); 30 | android.actionViewIntent( 31 | res.path(), 32 | 'application/vnd.android.package-archive' 33 | ); 34 | }); 35 | }; 36 | 37 | export default downloadApk; 38 | -------------------------------------------------------------------------------- /src/utils/downloadFile.ts: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs'; 2 | import RNFetchBlob from 'rn-fetch-blob'; 3 | import slugify from './slugify'; 4 | import { requestWriteExternalStoragePermission } from '../hooks/useBackup'; 5 | 6 | export interface DownloadFileParams { 7 | url: string; 8 | fileName: string; 9 | dir?: string; 10 | } 11 | 12 | const downloadFile = async ({ 13 | url, 14 | fileName, 15 | dir = RNFetchBlob.fs.dirs.MusicDir 16 | }: DownloadFileParams): Promise => { 17 | await requestWriteExternalStoragePermission(); 18 | 19 | return RNFetchBlob.config({ 20 | addAndroidDownloads: { 21 | useDownloadManager: true, 22 | title: fileName, 23 | path: `${dir}/${slugify(fileName)}.mp4`, 24 | mime: 'video/mp4', 25 | notification: true 26 | } 27 | }) 28 | .fetch('GET', url) 29 | .then(res => { 30 | actions.setSnackbar({ 31 | message: `File has been download in your Music folder` 32 | }); 33 | }); 34 | }; 35 | 36 | export default downloadFile; 37 | -------------------------------------------------------------------------------- /src/utils/fetchGithubAppVersion.ts: -------------------------------------------------------------------------------- 1 | interface FetchHopRelease { 2 | tagName: string; 3 | browserDownloadUrl: string; 4 | } 5 | 6 | const API_GITHUB_YAP_RELEASE = 7 | 'https://api.github.com/repos/stephane-r/HoloPlay/releases'; 8 | 9 | const fetchHopRelease = (): FetchHopRelease => 10 | fetch(API_GITHUB_YAP_RELEASE) 11 | .then((request) => request.json()) 12 | .then(([repo]) => ({ 13 | tagName: repo.tag_name, 14 | browserDownloadUrl: repo.assets[0].browser_download_url 15 | })); 16 | 17 | export default fetchHopRelease; 18 | -------------------------------------------------------------------------------- /src/utils/fetchInvidiousInstances.ts: -------------------------------------------------------------------------------- 1 | import { Instance } from '../types'; 2 | 3 | const fetchInvidiousInstances = (): Promise => 4 | fetch('https://instances.invidio.us/instances.json') 5 | .then((response) => response.json()) 6 | .then((result) => { 7 | let instances = []; 8 | 9 | result.forEach((instance) => { 10 | instances = [...instances, instance[1]]; 11 | }); 12 | 13 | return instances; 14 | }); 15 | 16 | export default fetchInvidiousInstances; 17 | -------------------------------------------------------------------------------- /src/utils/fetchPlaylists.ts: -------------------------------------------------------------------------------- 1 | import callApi from './callApi'; 2 | import { Playlist } from '../types'; 3 | import { actions } from '../store'; 4 | import { ApiRoutes, FAVORIS_PLAYLIST_TITLE } from '../constants'; 5 | 6 | const fetchPlaylists = async (): Promise => { 7 | const playlists: Playlist[] = await callApi({ 8 | url: ApiRoutes.Playlists 9 | }); 10 | 11 | if (playlists.error) { 12 | throw new Error(playlists.error); 13 | } 14 | 15 | const favorisPlaylist = playlists.find( 16 | (p) => p.title === FAVORIS_PLAYLIST_TITLE 17 | ); 18 | 19 | actions.receivePlaylists(playlists); 20 | 21 | if (favorisPlaylist) { 22 | actions.receiveFavorisPlaylist(favorisPlaylist); 23 | } 24 | 25 | return playlists; 26 | }; 27 | 28 | export default fetchPlaylists; 29 | -------------------------------------------------------------------------------- /src/utils/formatTimeUnit.ts: -------------------------------------------------------------------------------- 1 | const formatTimeUnit = (input: string, unit: string): string => { 2 | const index: number = input.indexOf(unit); 3 | const output = '00'; 4 | 5 | if (index < 0) { 6 | return output; 7 | } 8 | 9 | // @ts-ignore 10 | if (isNaN(input.charAt(index - 2))) { 11 | return '0' + input.charAt(index - 1); 12 | } 13 | 14 | return input.charAt(index - 2) + input.charAt(index - 1); 15 | }; 16 | 17 | export default formatTimeUnit; 18 | -------------------------------------------------------------------------------- /src/utils/getLanguageName.ts: -------------------------------------------------------------------------------- 1 | const getLanguageName = (lng: 'en' | 'fr' | 'cs'): string => { 2 | let language; 3 | 4 | switch (true) { 5 | case lng === 'fr': 6 | language = 'Français'; 7 | break; 8 | case lng === 'cs': 9 | language = 'Čeština'; 10 | break; 11 | default: 12 | language = 'English'; 13 | break; 14 | } 15 | 16 | return language; 17 | }; 18 | 19 | export default getLanguageName; 20 | -------------------------------------------------------------------------------- /src/utils/hex2rgba.ts: -------------------------------------------------------------------------------- 1 | const hex2rgba = (hex: string, alpha: number = 1) => { 2 | const [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16)); 3 | 4 | return `rgba(${r},${g},${b},${alpha})`; 5 | }; 6 | 7 | export default hex2rgba; 8 | -------------------------------------------------------------------------------- /src/utils/invidiousSearch.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import { SearchVideo } from '../types'; 3 | 4 | const invidiousSearch = async (value: string): Promise => { 5 | const instance = await AsyncStorage.getItem('instance'); 6 | 7 | return fetch(`${instance}/api/v1/search?q=${value}&type=video`) 8 | .then(response => response.json()) 9 | .then(result => result); 10 | }; 11 | 12 | export default invidiousSearch; 13 | -------------------------------------------------------------------------------- /src/utils/slugify.ts: -------------------------------------------------------------------------------- 1 | const slugify = (text: string): string => 2 | text 3 | .toString() 4 | .toLowerCase() 5 | .replace(/\s+/g, '-') 6 | .replace(/[^\w\-]+/g, '') 7 | .replace(/\-\-+/g, '-') 8 | .replace(/^-+/, '') 9 | .replace(/-+$/, ''); 10 | 11 | export default slugify; 12 | -------------------------------------------------------------------------------- /src/utils/stripTrailingSlash.ts: -------------------------------------------------------------------------------- 1 | const stripTrailingSlash = (str: string): string => 2 | str.endsWith('/') ? str.slice(0, -1) : str; 3 | 4 | export default stripTrailingSlash; 5 | -------------------------------------------------------------------------------- /src/utils/youtubeDurationToSeconds.ts: -------------------------------------------------------------------------------- 1 | const youtubeDurationToSeconds = (value: string): number => { 2 | let hours = 0; 3 | let minutes = 0; 4 | let seconds = 0; 5 | let minutes_split = null; 6 | let seconds_split = null; 7 | let hours_split = null; 8 | let duration = value; 9 | 10 | // Remove PT from string ref: https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration 11 | duration = duration.replace('PT', ''); 12 | 13 | // If the string contains hours parse it and remove it from our duration string 14 | if (duration.indexOf('H') > -1) { 15 | hours_split = duration.split('H'); 16 | hours = parseInt(hours_split[0]); 17 | duration = hours_split[1]; 18 | } 19 | 20 | // If the string contains minutes parse it and remove it from our duration string 21 | if (duration.indexOf('M') > -1) { 22 | minutes_split = duration.split('M'); 23 | minutes = parseInt(minutes_split[0]); 24 | duration = minutes_split[1]; 25 | } 26 | 27 | // If the string contains seconds parse it and remove it from our duration string 28 | if (duration.indexOf('S') > -1) { 29 | seconds_split = duration.split('S'); 30 | seconds = parseInt(seconds_split[0]); 31 | } 32 | 33 | // Math the values to return seconds 34 | return hours * 60 * 60 + minutes * 60 + seconds; 35 | }; 36 | 37 | export default youtubeDurationToSeconds; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "react", 8 | "lib": ["ES2017", "ESNext"], 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "strict": true, 12 | "target": "esnext" 13 | }, 14 | "exclude": ["node_modules", "babel.config.js", "metro.config.js"] 15 | } 16 | --------------------------------------------------------------------------------