├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------