Compare commits

...

37 Commits

Author SHA1 Message Date
Lei Nelissen
287b64c356 chore: release v2.1.0 2023-04-23 23:51:53 +02:00
Lei Nelissen
e116e95236 fix: reign in padding on album view a bit 2023-04-23 23:37:48 +02:00
Lei Nelissen
c8283fc580 feat: finish offsets on new navigation views 2023-04-23 23:31:35 +02:00
Lei Nelissen
81b9ba683a fix: make similar albums translateable 2023-04-23 22:06:39 +02:00
Lei Nelissen
913d185b46 fix: padding in similar scrollwheel 2023-04-23 01:29:59 +02:00
Lei Nelissen
1d97830f83 fix: contentInset doesn't behave on Android 2023-04-23 01:25:43 +02:00
Lei Nelissen
6ccfd19dea fix: linter issues 2023-04-23 01:15:07 +02:00
Lei Nelissen
2e816f4a71 fix: correctly calculate amount of minutes when an hour is present 2023-04-23 01:14:56 +02:00
Lei Nelissen
4ff071d0c8 fix: only show similar albums if there are any 2023-04-23 01:07:56 +02:00
Lei Nelissen
dba87247d8 feat: add extra metadata to the album view 2023-04-23 01:04:30 +02:00
Lei Nelissen
c3c32ae565 feat: show artist in playlist view 2023-04-22 23:41:41 +02:00
Lei Nelissen
1d7db11328 fix: also add navigator padding when playing the first track in a queue 2023-04-22 23:41:25 +02:00
Lei Nelissen
1a5e4aee12 feat: add blurview to headers as well 2023-04-22 23:31:37 +02:00
Lei Nelissen
e2c1c0300f fix: keep album views in search tab when navigating from search results 2023-04-22 22:31:54 +02:00
Lei Nelissen
7601408d49 feat: update tab bars with blurview 2023-04-22 21:58:27 +02:00
Lei Nelissen
4509ef1ec6 fix: remove padding from Modal 2023-04-22 20:52:30 +02:00
Lei Nelissen
dcd3f595ed chore: update changelog 2023-04-12 11:50:35 +02:00
Lei Nelissen
c704a27c09 chore: release v2.0.5 2023-04-12 11:47:05 +02:00
Lei Nelissen
67499b1103 fix: crash when fast-image fails to load an image
There is some weird behaviour in react-native-fast-image
which causes it to crash whenever the onError or
onLoadEnd prop is set. The issue is documented in
https://github.com/DylanVann/react-native-fast-image/issues/504.
2023-04-12 11:31:33 +02:00
Lei Nelissen
5bbc3b8588 chore: update badges 2023-04-11 22:43:01 +02:00
Lei Nelissen
f71c46eacc chore: add links to README badges 2023-04-11 22:33:32 +02:00
Lei Nelissen
e4b75ce0a2 chore: fix README whitespace 2023-04-11 22:29:39 +02:00
Lei Nelissen
d45195dc6b chore: update README 2023-04-11 22:29:01 +02:00
Lei Nelissen
4419b387e9 chore: iOS build for v2.0.4 2023-04-11 21:12:41 +02:00
Lei Nelissen
63a755a231 chore: fix changelog 2023-04-11 18:46:21 +02:00
Lei Nelissen
1d7b3d7e6c chore: generate changelog 2023-04-11 18:45:36 +02:00
Lei Nelissen
531c6f708d chore: release v2.0.4 2023-04-11 18:39:37 +02:00
Lei Nelissen
56647cd7ab chore: add Android screenshots 2023-04-11 18:34:12 +02:00
Lei Nelissen
1648389ccc fix: disable BlurView on Android as it crashes the app 2023-04-11 18:27:55 +02:00
Lei Nelissen
a532154ce0 fix: use debug signing config when not having a keystore 2023-04-11 10:48:24 +02:00
Lei Nelissen
74d82eb77a fix: only set signingConfig to release when a keystore is available 2023-04-10 17:42:51 +02:00
Lei Nelissen
a8c0003fc1 fix: linter issue 2023-04-10 17:15:32 +02:00
Lei Nelissen
ba805e061e feat: Add base Android content for F-Droid and Play Store 2023-04-10 17:10:53 +02:00
Lei Nelissen
cc14373575 feat: setup Fastlane for Google Play Store 2023-04-10 17:10:12 +02:00
Lei Nelissen
943815e4a6 chore: update fastlane 2023-04-10 17:09:24 +02:00
Lei Nelissen
2f45f868c8 fix: linting issue 2023-03-08 10:09:59 +01:00
Lei Nelissen
0a0c78f3d5 feat: add fallback images when album cover isn't available 2023-03-07 23:03:09 +01:00
67 changed files with 2175 additions and 1488 deletions

View File

@@ -52,6 +52,12 @@ module.exports = {
'react/prop-types': 'off', 'react/prop-types': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'error' 'error'
],
'react/jsx-no-literals': [
'error',
{
ignoreProps: true
}
] ]
}, },
settings: { settings: {

3
.gitignore vendored
View File

@@ -72,4 +72,5 @@ certificates/
sentry.properties sentry.properties
screenshots screenshots
fastlane/Preview.html fastlane/Preview.html
fastlane/play-store-credentials.json

View File

@@ -1,3 +1,32 @@
## [2.0.5](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.0.4...v2.0.5) (2023-04-12)
### Bug Fixes
* crash when fast-image fails to load an image ([67499b1](https://github.com/leinelissen/jellyfin-audio-player/commit/67499b11037779bf33bb557fff69114cd519c78e))
## [2.0.4](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.0.3...v2.0.4) (2023-04-11)
### Bug Fixes
* disable BlurView on Android as it crashes the app ([1648389](https://github.com/leinelissen/jellyfin-audio-player/commit/1648389ccce088e6836bcad31bd5c3b7cb996a78))
* linter issue ([a8c0003](https://github.com/leinelissen/jellyfin-audio-player/commit/a8c0003fc13cb7d4778f65e8702b1c3c5fd1cc59))
* linting issue ([2f45f86](https://github.com/leinelissen/jellyfin-audio-player/commit/2f45f868c8cc8a7f4308282b672d1d487f480c0a))
* only set signingConfig to release when a keystore is available ([74d82eb](https://github.com/leinelissen/jellyfin-audio-player/commit/74d82eb77a412ba84d0820abbad84ac304c62611))
* use debug signing config when not having a keystore ([a532154](https://github.com/leinelissen/jellyfin-audio-player/commit/a532154ce023ba2eecbbc3c8d7bbe08bcca0cd57))
### Features
* Add base Android content for F-Droid and Play Store ([ba805e0](https://github.com/leinelissen/jellyfin-audio-player/commit/ba805e061e56d719b18cfd8a6bafccf9174110b8))
* add fallback images when album cover isn't available ([0a0c78f](https://github.com/leinelissen/jellyfin-audio-player/commit/0a0c78f3d592e0d92a6bb3fd605810e0af1441bb))
* setup Fastlane for Google Play Store ([cc14373](https://github.com/leinelissen/jellyfin-audio-player/commit/cc14373575a844458737ac6f0a6e8d8ea783ce75))
## [2.0.3](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.0.2...v2.0.3) (2023-02-28) ## [2.0.3](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.0.2...v2.0.3) (2023-02-28)

View File

@@ -1,7 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.5) CFPropertyList (3.0.6)
rexml rexml
activesupport (6.1.6) activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -9,7 +9,7 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.1) addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5) algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
@@ -17,16 +17,16 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.660.0) aws-partitions (1.743.0)
aws-sdk-core (3.167.0) aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.59.0) aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.1) aws-sdk-s3 (1.120.1)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
@@ -86,8 +86,8 @@ GEM
escape (0.0.4) escape (0.0.4)
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
excon (0.94.0) excon (0.99.0)
faraday (1.10.2) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@@ -116,7 +116,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.211.0) fastlane (2.212.1)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@@ -163,9 +163,9 @@ GEM
fourflusher (2.3.1) fourflusher (2.3.1)
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.31.0) google-apis-androidpublisher_v3 (0.38.0)
google-apis-core (>= 0.9.1, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.9.1) google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@@ -174,10 +174,10 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.16.0) google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.9.1, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.12.0) google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.9.1, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0) google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
@@ -185,7 +185,7 @@ GEM
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0) google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0) google-cloud-storage (1.44.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@@ -194,7 +194,7 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.3.0) googleauth (1.5.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@@ -207,11 +207,11 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.10.0) i18n (1.10.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jmespath (1.6.1) jmespath (1.6.2)
json (2.6.2) json (2.6.3)
jwt (2.5.0) jwt (2.7.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.15.0) minitest (5.15.0)
molinillo (0.8.0) molinillo (0.8.0)
@@ -223,7 +223,7 @@ GEM
netrc (0.11.0) netrc (0.11.0)
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.7.0)
public_suffix (4.0.7) public_suffix (4.0.7)
rake (13.0.6) rake (13.0.6)
representable (3.2.0) representable (3.2.0)
@@ -242,7 +242,7 @@ GEM
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.8) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
@@ -262,7 +262,7 @@ GEM
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
webrick (1.7.0) webrick (1.8.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.22.0) xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)

View File

@@ -1,34 +1,33 @@
# Jellyfin Audio Player <div align="center">
![Fastlane](https://github.com/leinelissen/jellyfin-audio-player/workflows/Fastlane/badge.svg)
![MIT License](https://img.shields.io/github/license/leinelissen/jellyfin-audio-player)
This is a [React Native](https://reactnative.dev/)-based audio streaming app for [Jellyfin](https://jellyfin.org/). Jellyfin is a community-based piece of software that allows you to stream your media library over the internet. By means of React Native, Jellyfin Audio Player allows you to stream your Jellyfin Music library, with full support for background audio and casting (ie. Airplay and Chromecast). ![Fintunes](./docs/images/fintunes-banner.png)
## ❗Now open for beta testing on iOS [![Get Fintunes on the App Store](./docs/images/app-store.svg)](https://apple.co/3MFYIJH)
Please follow this link to enroll for the TestFlight beta release of Jellyfin Audio Player: https://testflight.apple.com/join/cf2AMDpx.
[![Latest GitHub release](https://img.shields.io/github/v/release/leinelissen/jellyfin-audio-player?label=latest%20release)](https://github.com/leinelissen/jellyfin-audio-player/releases/latest)
[![Latest App Store release](https://img.shields.io/itunes/v/1527732194?label=app%20store)](https://apple.co/3MFYIJH)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/leinelissen/jellyfin-audio-player/fastlane.yml)
[![MIT License](https://img.shields.io/github/license/leinelissen/jellyfin-audio-player)](./LICENSE.md)
[![Discord](https://img.shields.io/discord/1080781083882307594)](https://discord.gg/xyd97GpC4Q)
<br />
With Fintunes, you can stream your Jellyfin audio library in full quality. List or search through your favourite tracks, albums and playlists. Stream to speakers and TVs wirelessly via either **AirPlay** or **Chromecast**. **Download** your favourite tracks and play them back, even when you are offline. Available in both a **dark and a light mode**, based on your operating system settings.
<br />
<br />
</div>
|![](./docs/images/now-playing.png)|![](./docs/images/recent-albums.png)|![](./docs/images/album-list.png) |![](./docs/images/now-playing.png)|![](./docs/images/recent-albums.png)|![](./docs/images/album-list.png)
|-|-|-| |-|-|-|
|![](./docs/images/album.png)|![](./docs/images/downloads.png)|![](./docs/images/search.png) |![](./docs/images/album.png)|![](./docs/images/downloads.png)|![](./docs/images/search.png)
## Features
* Sorting by recent albums
* Browsing through all available albums
* Searching based on album and artist names
* Queuing tracks and albums
* AirPlay and Chromecast support
* Background audio
* Native Dark Mode
* Downloading music for offline playback
* Searching based on track names
* Looping and shuffling queue
## Getting Started ## Getting Started
This piece of software is in beta. I am working on getting this app in ~~TestFlight and~~ Google Play Developer Console, but this is contingent on keys being available. In the meantime, IPAs and APK are intermittenly released on the [Releases page](https://github.com/leinelissen/jellyfin-audio-player/releases). Alternatively, you can build this app from source using the build instructions. Fintunes is available on the [App Store](https://apple.co/3MFYIJH). It is in the process of being released on both Google Play and F-Droid. You can also grab either an APK or IPA from the [release page](https://github.com/leinelissen/jellyfin-audio-player/releases/latest). If you are feeling frisky, you can compile Fintunes from source using the settings below.
### Using the app ### Using the app
You will need to setup your Jellyfin account for the application to be able to pull in all your audio. To do this, go over to the "Settings" tab and click the "Set Jellyfin server"-button. A modal will pop up in which you will enter your Jellyfin server url, after which you enter your credentials in the provided browser view. When the app detects your credentials, they will automatically be remembered for the future. You will need to setup your Jellyfin account for the application to be able to pull in all your audio. To do this, go over to the "Settings" tab and click the "Set Jellyfin server"-button. A modal will pop up in which you will enter your Jellyfin server URL, after which you enter your credentials in the provided browser view. When the app detects your credentials, they will automatically be remembered for the future.
## Building from source ## Building from source
### Prerequisites ### Prerequisites

View File

@@ -138,8 +138,8 @@ android {
applicationId "nl.moeilijkedingen.jellyfinaudioplayer" applicationId "nl.moeilijkedingen.jellyfinaudioplayer"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15 versionCode 18
versionName "2.0.3" versionName "2.1.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) { if (isNewArchitectureEnabled()) {
@@ -217,6 +217,14 @@ android {
keyAlias 'androiddebugkey' keyAlias 'androiddebugkey'
keyPassword 'android' keyPassword 'android'
} }
release {
if (project.hasProperty('FINTUNES_UPLOAD_STORE_FILE')) {
storeFile file(FINTUNES_UPLOAD_STORE_FILE)
storePassword FINTUNES_UPLOAD_STORE_PASSWORD
keyAlias FINTUNES_UPLOAD_KEY_ALIAS
keyPassword FINTUNES_UPLOAD_KEY_PASSWORD
}
}
} }
buildTypes { buildTypes {
debug { debug {
@@ -225,7 +233,11 @@ android {
release { release {
// Caution! In production, you need to generate your own keystore file. // Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android. // see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug if (project.hasProperty('FINTUNES_UPLOAD_STORE_FILE')) {
signingConfig signingConfigs.release
} else {
signingConfig signingConfigs.debug
}
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
} }

46
docs/images/app-store.svg Executable file
View File

@@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

View File

@@ -1,4 +1,5 @@
package_name("nl.moeilijkedingen.jellyfinaudioplayer") package_name("nl.moeilijkedingen.jellyfinaudioplayer")
app_identifier("nl.moeilijkedingen.jellyfinaudioplayer") app_identifier("nl.moeilijkedingen.jellyfinaudioplayer")
apple_id("lei@moeilijkedingen.nl") apple_id("lei@moeilijkedingen.nl")
team_id("238P3C58WC") team_id("238P3C58WC")
json_key_file("./fastlane/play-store-credentials.json")

View File

@@ -44,6 +44,15 @@ platform :ios do
) )
upload_to_testflight() upload_to_testflight()
end end
lane :build do
build_app(
scheme: "Fintunes",
output_directory: "build",
workspace: "ios/Fintunes.xcworkspace",
export_method: "app-store",
)
end
after_all do after_all do
build_number = get_build_number( build_number = get_build_number(
@@ -101,8 +110,18 @@ platform :android do
gradle_file: "android/app/build.gradle" gradle_file: "android/app/build.gradle"
) )
gradle( gradle(
task: "assembleRelease", task: "assemble",
build_type: "Release",
project_dir: "android" project_dir: "android"
) )
end end
lane :release do
gradle(
task: "bundle",
build_type: 'Release',
project_dir: "android"
)
upload_to_play_store
end
end end

View File

@@ -31,6 +31,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
### ios build
```sh
[bundle exec] fastlane ios build
```
### ios screenshots ### ios screenshots
```sh ```sh
@@ -52,6 +60,22 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
Generate beta build Generate beta build
### android release
```sh
[bundle exec] fastlane android release
```
### android build
```sh
[bundle exec] fastlane android build
```
---- ----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -0,0 +1 @@
Fintunes is a streaming audio player for the Jellyfin media system. It features a gorgeous interface that allows you to play your favourite music with ease. You can search your entire library for any track, or just take it easy with a playlist that you've created earlier in Jellyfin. All tracks are streamed directly at the highest quality from your Jellyfin library. Streaming not always an option? Any track in your Jellyfin library can be downloaded and played offline.

View File

@@ -0,0 +1 @@
Streaming audio player for Jellyfin

View File

@@ -0,0 +1 @@
Fintunes

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

@@ -606,7 +606,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53; CURRENT_PROJECT_VERSION = 64;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = 238P3C58WC;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
@@ -643,7 +643,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53; CURRENT_PROJECT_VERSION = 64;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = 238P3C58WC;
INFOPLIST_FILE = Fintunes/Info.plist; INFOPLIST_FILE = Fintunes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -799,7 +799,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 53; CURRENT_PROJECT_VERSION = 64;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = 238P3C58WC;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -832,7 +832,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 53; CURRENT_PROJECT_VERSION = 64;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = 238P3C58WC;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.3</string> <string>2.1.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>53</string> <string>64</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>

View File

@@ -2,14 +2,14 @@ PODS:
- boost (1.76.0) - boost (1.76.0)
- CocoaAsyncSocket (7.6.5) - CocoaAsyncSocket (7.6.5)
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- FBLazyVector (0.70.5) - FBLazyVector (0.70.8)
- FBReactNativeSpec (0.70.5): - FBReactNativeSpec (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTRequired (= 0.70.5) - RCTRequired (= 0.70.8)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Core (= 0.70.5) - React-Core (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- Flipper (0.164.0): - Flipper (0.164.0):
- Flipper-Folly (~> 2.6) - Flipper-Folly (~> 2.6)
- Flipper-Boost-iOSX (1.76.0.1.11) - Flipper-Boost-iOSX (1.76.0.1.11)
@@ -72,7 +72,7 @@ PODS:
- FlipperKit/FlipperKitNetworkPlugin - FlipperKit/FlipperKitNetworkPlugin
- fmt (6.2.1) - fmt (6.2.1)
- glog (0.3.5) - glog (0.3.5)
- hermes-engine (0.70.5) - hermes-engine (0.70.8)
- libevent (2.1.12) - libevent (2.1.12)
- libwebp (1.2.4): - libwebp (1.2.4):
- libwebp/demux (= 1.2.4) - libwebp/demux (= 1.2.4)
@@ -101,216 +101,216 @@ PODS:
- fmt (~> 6.2.1) - fmt (~> 6.2.1)
- glog - glog
- libevent - libevent
- RCTRequired (0.70.5) - RCTRequired (0.70.8)
- RCTTypeSafety (0.70.5): - RCTTypeSafety (0.70.8):
- FBLazyVector (= 0.70.5) - FBLazyVector (= 0.70.8)
- RCTRequired (= 0.70.5) - RCTRequired (= 0.70.8)
- React-Core (= 0.70.5) - React-Core (= 0.70.8)
- React (0.70.5): - React (0.70.8):
- React-Core (= 0.70.5) - React-Core (= 0.70.8)
- React-Core/DevSupport (= 0.70.5) - React-Core/DevSupport (= 0.70.8)
- React-Core/RCTWebSocket (= 0.70.5) - React-Core/RCTWebSocket (= 0.70.8)
- React-RCTActionSheet (= 0.70.5) - React-RCTActionSheet (= 0.70.8)
- React-RCTAnimation (= 0.70.5) - React-RCTAnimation (= 0.70.8)
- React-RCTBlob (= 0.70.5) - React-RCTBlob (= 0.70.8)
- React-RCTImage (= 0.70.5) - React-RCTImage (= 0.70.8)
- React-RCTLinking (= 0.70.5) - React-RCTLinking (= 0.70.8)
- React-RCTNetwork (= 0.70.5) - React-RCTNetwork (= 0.70.8)
- React-RCTSettings (= 0.70.5) - React-RCTSettings (= 0.70.8)
- React-RCTText (= 0.70.5) - React-RCTText (= 0.70.8)
- React-RCTVibration (= 0.70.5) - React-RCTVibration (= 0.70.8)
- react-airplay (1.2.0): - react-airplay (1.2.0):
- React-Core - React-Core
- React-bridging (0.70.5): - React-bridging (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-callinvoker (0.70.5) - React-callinvoker (0.70.8)
- React-Codegen (0.70.5): - React-Codegen (0.70.8):
- FBReactNativeSpec (= 0.70.5) - FBReactNativeSpec (= 0.70.8)
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTRequired (= 0.70.5) - RCTRequired (= 0.70.8)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Core (= 0.70.5) - React-Core (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-Core (0.70.5): - React-Core (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.70.5) - React-Core/Default (= 0.70.8)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/CoreModulesHeaders (0.70.5): - React-Core/CoreModulesHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/Default (0.70.5): - React-Core/Default (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/DevSupport (0.70.5): - React-Core/DevSupport (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.70.5) - React-Core/Default (= 0.70.8)
- React-Core/RCTWebSocket (= 0.70.5) - React-Core/RCTWebSocket (= 0.70.8)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-jsinspector (= 0.70.5) - React-jsinspector (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTActionSheetHeaders (0.70.5): - React-Core/RCTActionSheetHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTAnimationHeaders (0.70.5): - React-Core/RCTAnimationHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTBlobHeaders (0.70.5): - React-Core/RCTBlobHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTImageHeaders (0.70.5): - React-Core/RCTImageHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTLinkingHeaders (0.70.5): - React-Core/RCTLinkingHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTNetworkHeaders (0.70.5): - React-Core/RCTNetworkHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTSettingsHeaders (0.70.5): - React-Core/RCTSettingsHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTTextHeaders (0.70.5): - React-Core/RCTTextHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTVibrationHeaders (0.70.5): - React-Core/RCTVibrationHeaders (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default - React-Core/Default
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-Core/RCTWebSocket (0.70.5): - React-Core/RCTWebSocket (0.70.8):
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.70.5) - React-Core/Default (= 0.70.8)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- Yoga - Yoga
- React-CoreModules (0.70.5): - React-CoreModules (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/CoreModulesHeaders (= 0.70.5) - React-Core/CoreModulesHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-RCTImage (= 0.70.5) - React-RCTImage (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-cxxreact (0.70.5): - React-cxxreact (0.70.8):
- boost (= 1.76.0) - boost (= 1.76.0)
- DoubleConversion - DoubleConversion
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.70.5) - React-callinvoker (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsinspector (= 0.70.5) - React-jsinspector (= 0.70.8)
- React-logger (= 0.70.5) - React-logger (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- React-runtimeexecutor (= 0.70.5) - React-runtimeexecutor (= 0.70.8)
- React-hermes (0.70.5): - React-hermes (0.70.8):
- DoubleConversion - DoubleConversion
- glog - glog
- hermes-engine - hermes-engine
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCT-Folly/Futures (= 2021.07.22.00) - RCT-Folly/Futures (= 2021.07.22.00)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-jsiexecutor (= 0.70.5) - React-jsiexecutor (= 0.70.8)
- React-jsinspector (= 0.70.5) - React-jsinspector (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- React-jsi (0.70.5): - React-jsi (0.70.8):
- boost (= 1.76.0) - boost (= 1.76.0)
- DoubleConversion - DoubleConversion
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-jsi/Default (= 0.70.5) - React-jsi/Default (= 0.70.8)
- React-jsi/Default (0.70.5): - React-jsi/Default (0.70.8):
- boost (= 1.76.0) - boost (= 1.76.0)
- DoubleConversion - DoubleConversion
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-jsiexecutor (0.70.5): - React-jsiexecutor (0.70.8):
- DoubleConversion - DoubleConversion
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- React-jsinspector (0.70.5) - React-jsinspector (0.70.8)
- React-logger (0.70.5): - React-logger (0.70.8):
- glog - glog
- react-native-blur (4.3.0): - react-native-blur (4.3.0):
- React-Core - React-Core
@@ -358,72 +358,72 @@ PODS:
- SwiftAudioEx (= 0.15.3) - SwiftAudioEx (= 0.15.3)
- react-native-webview (11.23.1): - react-native-webview (11.23.1):
- React-Core - React-Core
- React-perflogger (0.70.5) - React-perflogger (0.70.8)
- React-RCTActionSheet (0.70.5): - React-RCTActionSheet (0.70.8):
- React-Core/RCTActionSheetHeaders (= 0.70.5) - React-Core/RCTActionSheetHeaders (= 0.70.8)
- React-RCTAnimation (0.70.5): - React-RCTAnimation (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTAnimationHeaders (= 0.70.5) - React-Core/RCTAnimationHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTBlob (0.70.5): - React-RCTBlob (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTBlobHeaders (= 0.70.5) - React-Core/RCTBlobHeaders (= 0.70.8)
- React-Core/RCTWebSocket (= 0.70.5) - React-Core/RCTWebSocket (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-RCTNetwork (= 0.70.5) - React-RCTNetwork (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTImage (0.70.5): - React-RCTImage (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTImageHeaders (= 0.70.5) - React-Core/RCTImageHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-RCTNetwork (= 0.70.5) - React-RCTNetwork (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTLinking (0.70.5): - React-RCTLinking (0.70.8):
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTLinkingHeaders (= 0.70.5) - React-Core/RCTLinkingHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTNetwork (0.70.5): - React-RCTNetwork (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTNetworkHeaders (= 0.70.5) - React-Core/RCTNetworkHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTSettings (0.70.5): - React-RCTSettings (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.70.5) - RCTTypeSafety (= 0.70.8)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTSettingsHeaders (= 0.70.5) - React-Core/RCTSettingsHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-RCTText (0.70.5): - React-RCTText (0.70.8):
- React-Core/RCTTextHeaders (= 0.70.5) - React-Core/RCTTextHeaders (= 0.70.8)
- React-RCTVibration (0.70.5): - React-RCTVibration (0.70.8):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.70.5) - React-Codegen (= 0.70.8)
- React-Core/RCTVibrationHeaders (= 0.70.5) - React-Core/RCTVibrationHeaders (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (= 0.70.5) - ReactCommon/turbomodule/core (= 0.70.8)
- React-runtimeexecutor (0.70.5): - React-runtimeexecutor (0.70.8):
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- ReactCommon/turbomodule/core (0.70.5): - ReactCommon/turbomodule/core (0.70.8):
- DoubleConversion - DoubleConversion
- glog - glog
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
- React-bridging (= 0.70.5) - React-bridging (= 0.70.8)
- React-callinvoker (= 0.70.5) - React-callinvoker (= 0.70.8)
- React-Core (= 0.70.5) - React-Core (= 0.70.8)
- React-cxxreact (= 0.70.5) - React-cxxreact (= 0.70.8)
- React-jsi (= 0.70.5) - React-jsi (= 0.70.8)
- React-logger (= 0.70.5) - React-logger (= 0.70.8)
- React-perflogger (= 0.70.5) - React-perflogger (= 0.70.8)
- RNCAsyncStorage (1.17.11): - RNCAsyncStorage (1.17.11):
- React-Core - React-Core
- RNFastImage (8.6.3): - RNFastImage (8.6.3):
@@ -432,11 +432,11 @@ PODS:
- SDWebImageWebPCoder (~> 0.8.4) - SDWebImageWebPCoder (~> 0.8.4)
- RNFS (2.20.0): - RNFS (2.20.0):
- React-Core - React-Core
- RNGestureHandler (2.8.0): - RNGestureHandler (2.9.0):
- React-Core - React-Core
- RNLocalize (2.2.4): - RNLocalize (2.2.4):
- React-Core - React-Core
- RNReanimated (2.12.0): - RNReanimated (2.14.4):
- DoubleConversion - DoubleConversion
- FBLazyVector - FBLazyVector
- FBReactNativeSpec - FBReactNativeSpec
@@ -695,8 +695,8 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234 boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: affa4ba1bfdaac110a789192f4d452b053a86624 FBLazyVector: ce6c993e675c5e9684e3b83aa0c346eb116c3ec6
FBReactNativeSpec: fe8b5f1429cfe83a8d72dc8ed61dc7704cac8745 FBReactNativeSpec: d8772db98ada3c2daf8f65e2105ada77bf209c02
Flipper: d08578a2cc23c60c27086b07930efaeb39101342 Flipper: d08578a2cc23c60c27086b07930efaeb39101342
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30
@@ -708,26 +708,26 @@ SPEC CHECKSUMS:
FlipperKit: ddf459d2625ca33f115492de5ba6d970e2576311 FlipperKit: ddf459d2625ca33f115492de5ba6d970e2576311
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 7fe5fc6ef707b7fdcb161b63898ec500e285653d hermes-engine: 0b19f33a9c2ec1dbdede3232606eeb1101db4cec
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: 21229f84411088e5d8538f21212de49e46cc83e2 RCTRequired: 35a7977a5a3cb2d3830c3fef7352b7b116115829
RCTTypeSafety: 62eed57a32924b09edaaf170a548d1fc96223086 RCTTypeSafety: 259790fb8b16c94e57e0d3d1e2479e69a2b93f50
React: f0254ccddeeef1defe66c6b1bb9133a4f040792b React: 89f0551b8f7a555e38ce016a81c50bc68f1972a8
react-airplay: 8197767f12cae11a7623b1507d29a89482a720ad react-airplay: 8197767f12cae11a7623b1507d29a89482a720ad
React-bridging: e46911666b7ec19538a620a221d6396cd293d687 React-bridging: 2e425b6bc8536206918fa55bf9dd37016f99bb33
React-callinvoker: 66b62e2c34546546b2f21ab0b7670346410a2b53 React-callinvoker: 1c733126b1e4d95d0d412d95c51cedf06b3b979d
React-Codegen: b6999435966df3bdf82afa3f319ba0d6f9a8532a React-Codegen: 41d2ddcd966eac2a5f2698d5cd21e3d741e999bd
React-Core: dabbc9d1fe0a11d884e6ee1599789cf8eb1058a5 React-Core: 3021f04b6b1a2064952e166470a58db671ed65b1
React-CoreModules: 5b6b7668f156f73a56420df9ec68ca2ec8f2e818 React-CoreModules: f569f295874d0864bfd7a686dad3f828f4e8813a
React-cxxreact: c7ca2baee46db22a30fce9e639277add3c3f6ad1 React-cxxreact: a6c952ae24061777510f7e60b808b673e624009e
React-hermes: c93e1d759ad5560dfea54d233013d7d2c725c286 React-hermes: be32d1db90d052cc025a38ec2ea4e1a493d33c6a
React-jsi: a565dcb49130ed20877a9bb1105ffeecbb93d02d React-jsi: 3d7bafe69dddd780fb3527b7f939dfcbfd6790b5
React-jsiexecutor: 31564fa6912459921568e8b0e49024285a4d584b React-jsiexecutor: bc8556d76f83a1a9075cdee207aad7c0b7b30a33
React-jsinspector: badd81696361249893a80477983e697aab3c1a34 React-jsinspector: 5e5497c844f2381e8648ec3a7d0ad25b3f27f23e
React-logger: fdda34dd285bdb0232e059b19d9606fa0ec3bb9c React-logger: b277ad8f4473f2506fb30b762b6348534a3de10e
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-flipper: c33a4995958ef12a2b2f8290d63bed7adeed7634 react-native-flipper: c33a4995958ef12a2b2f8290d63bed7adeed7634
react-native-netinfo: f80db8cac2151405633324cb645c60af098ee461 react-native-netinfo: f80db8cac2151405633324cb645c60af098ee461
@@ -735,24 +735,24 @@ SPEC CHECKSUMS:
react-native-skia: 7f9a3bd36c4247005e87005d912dcf6db76a6289 react-native-skia: 7f9a3bd36c4247005e87005d912dcf6db76a6289
react-native-track-player: 0c26d981b5097910486cbbeb6d8f5352d41be069 react-native-track-player: 0c26d981b5097910486cbbeb6d8f5352d41be069
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581 react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
React-perflogger: e68d3795cf5d247a0379735cbac7309adf2fb931 React-perflogger: e9249a18e055cae96fdf685bf6145cbea62506c8
React-RCTActionSheet: 05452c3b281edb27850253db13ecd4c5a65bc247 React-RCTActionSheet: a6d2a544a4605a111ce80fa9319cc870ca3ea778
React-RCTAnimation: 578eebac706428e68466118e84aeacf3a282b4da React-RCTAnimation: 21b776b15aa5451a0b5bcb342fd2f346817c1101
React-RCTBlob: f47a0aa61e7d1fb1a0e13da832b0da934939d71a React-RCTBlob: 95f54d45305b4103b29d8b2c1e705b5c3183239a
React-RCTImage: 60f54b66eed65d86b6dffaf4733d09161d44929d React-RCTImage: 1b76ab9e3b60313edd85bc3fd3e07c29cec6ab68
React-RCTLinking: 91073205aeec4b29450ca79b709277319368ac9e React-RCTLinking: 7176da2a80f3056152a51587812d6d0c451b1f7b
React-RCTNetwork: ca91f2c9465a7e335c8a5fae731fd7f10572213b React-RCTNetwork: d36f896304e6ef2998f58cd4199a0239bd312318
React-RCTSettings: 1a9a5d01337d55c18168c1abe0f4a589167d134a React-RCTSettings: 004b9a1afb5870f4bcd06521c088e738c1558940
React-RCTText: c591e8bd9347a294d8416357ca12d779afec01d5 React-RCTText: a2606a79fdb52dd2bde0d7fde7726160fa16b70c
React-RCTVibration: 8e5c8c5d17af641f306d7380d8d0fe9b3c142c48 React-RCTVibration: 19d21a3ed620352180800447771f68a101f196e9
React-runtimeexecutor: 7401c4a40f8728fd89df4a56104541b760876117 React-runtimeexecutor: f795fd426264709901c09432c6ce072f8400147e
ReactCommon: c9246996e73bf75a2c6c3ff15f1e16707cdc2da9 ReactCommon: c440e7f15075e81eb29802521c58a1f38b1aa903
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60 RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNLocalize: 0df7970cfc60389f00eb62fd7c097dc75af3fb4f RNLocalize: 0df7970cfc60389f00eb62fd7c097dc75af3fb4f
RNReanimated: 2a91e85fcd343f8af3c58d3425b99fdd285590a5 RNReanimated: 6668b0587bebd4b15dd849b99e5a9c70fc12ed95
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
RNSentry: db7fd7b66efda28885e4e904a8b5e7349aec61c1 RNSentry: db7fd7b66efda28885e4e904a8b5e7349aec61c1
RNSVG: 38ca962c970dbce1ca38991a5aebf26d163f9efb RNSVG: 38ca962c970dbce1ca38991a5aebf26d163f9efb
@@ -761,9 +761,9 @@ SPEC CHECKSUMS:
Sentry: 4272663eb0eda312024d795ca3f5a562a8ce5e18 Sentry: 4272663eb0eda312024d795ca3f5a562a8ce5e18
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
SwiftAudioEx: 83eabba2940924fc1c0d5cb0896049921365229c SwiftAudioEx: 83eabba2940924fc1c0d5cb0896049921365229c
Yoga: eca980a5771bf114c41a754098cd85e6e0d90ed7 Yoga: d6133108734e69e8c0becc6ba587294b94829687
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: 94434618afff1be257dd0576e9a75bcaa7b48664 PODFILE CHECKSUM: 94434618afff1be257dd0576e9a75bcaa7b48664
COCOAPODS: 1.11.3 COCOAPODS: 1.12.0

1460
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "fintunes", "name": "fintunes",
"version": "2.0.3", "version": "2.1.0",
"main": "src/index.js", "main": "src/index.js",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -8,14 +8,16 @@
"ios": "react-native run-ios --scheme \"Fintunes\"", "ios": "react-native run-ios --scheme \"Fintunes\"",
"start": "react-native start", "start": "react-native start",
"test": "jest", "test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx && tsc --noEmit", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
"build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'" "build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'",
"postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^1.17.11", "@react-native-async-storage/async-storage": "^1.17.11",
"@react-native-community/blur": "^4.3.0", "@react-native-community/blur": "^4.3.0",
"@react-native-community/netinfo": "^9.3.6", "@react-native-community/netinfo": "^9.3.6",
"@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/bottom-tabs": "^6.4.0",
"@react-navigation/elements": "^1.3.17",
"@react-navigation/native": "^6.0.13", "@react-navigation/native": "^6.0.13",
"@react-navigation/native-stack": "^6.9.1", "@react-navigation/native-stack": "^6.9.1",
"@react-navigation/stack": "^6.3.4", "@react-navigation/stack": "^6.3.4",
@@ -29,17 +31,18 @@
"hermes-engine": "^0.11.0", "hermes-engine": "^0.11.0",
"i18n-js": "^3.9.2", "i18n-js": "^3.9.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"patch-package": "^6.5.1",
"react": "18.2.0", "react": "18.2.0",
"react-airplay": "^1.2.0", "react-airplay": "^1.2.0",
"react-native": "0.70.5", "react-native": "^0.70.8",
"react-native-collapsible": "^1.6.0", "react-native-collapsible": "^1.6.0",
"react-native-dotenv": "^3.4.2", "react-native-dotenv": "^3.4.2",
"react-native-fast-image": "^8.6.3", "react-native-fast-image": "^8.6.3",
"react-native-flipper": "^0.164.0", "react-native-flipper": "^0.164.0",
"react-native-fs": "^2.20.0", "react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.8.0", "react-native-gesture-handler": "^2.9.0",
"react-native-localize": "^2.2.4", "react-native-localize": "^2.2.4",
"react-native-reanimated": "^2.12.0", "react-native-reanimated": "^2.14.4",
"react-native-safe-area-context": "^4.4.1", "react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.18.2", "react-native-screens": "^3.18.2",
"react-native-shadow-2": "^7.0.6", "react-native-shadow-2": "^7.0.6",

View File

@@ -0,0 +1,22 @@
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
index f710081..87c4532 100644
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
@@ -32,7 +32,7 @@
- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
_onFastImageLoadEnd = onFastImageLoadEnd;
- if (self.hasCompleted) {
+ if (self.hasCompleted && _onFastImageLoadEnd != NULL) {
_onFastImageLoadEnd(@{});
}
}
@@ -46,7 +46,7 @@
- (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
_onFastImageError = onFastImageError;
- if (self.hasErrored) {
+ if (self.hasErrored && _onFastImageError != NULL) {
_onFastImageError(@{});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

View File

@@ -2,7 +2,7 @@ import { BlurView, BlurViewProps } from '@react-native-community/blur';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
import { ColorSchemeName, Platform, StyleSheet, useColorScheme } from 'react-native'; import { ColorSchemeName, Platform, StyleSheet, View, useColorScheme } from 'react-native';
const majorPlatformVersion = typeof Platform.Version === 'string' ? parseInt(Platform.Version, 10) : Platform.Version; const majorPlatformVersion = typeof Platform.Version === 'string' ? parseInt(Platform.Version, 10) : Platform.Version;
@@ -108,14 +108,8 @@ export function ColoredBlurView(props: PropsWithChildren<BlurViewProps>) {
: scheme === 'dark' ? 'extraDark' : 'xlight' : scheme === 'dark' ? 'extraDark' : 'xlight'
} /> } />
) : ( ) : (
<BlurView <View {...props} style={[ props.style, {
{...props} backgroundColor: scheme === 'light' ? '#f6f6f6fb' : '#333333fb',
blurType={scheme === 'dark' ? 'dark' : 'light'} } ]} />
blurAmount={10}
style={[ props.style, {
backgroundColor: scheme === 'light' ? '#f6f6f6bb' : '#333333bb',
borderRadius: 8
} ]}
/>
); );
} }

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Dimensions, ViewProps } from 'react-native'; import { Dimensions, useColorScheme, ViewProps } from 'react-native';
import { Canvas, Blur, Image as SkiaImage, useImage, Offset, Mask, RoundedRect, Shadow } from '@shopify/react-native-skia'; import { Canvas, Blur, Image as SkiaImage, useImage, Offset, Mask, RoundedRect, Shadow } from '@shopify/react-native-skia';
import useDefaultStyles from './Colors'; import useDefaultStyles from './Colors';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
@@ -45,14 +45,18 @@ function CoverImage({
src, src,
}: Props) { }: Props) {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const colorScheme = useColorScheme();
const image = useImage(src || ''); const image = useImage(src || null);
const fallback = useImage(colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png'));
const { canvasSize, imageSize } = useMemo(() => { const { canvasSize, imageSize } = useMemo(() => {
const imageSize = Screen.width - margin; const imageSize = Screen.width - margin;
const canvasSize = imageSize + blurRadius * 2; const canvasSize = imageSize + blurRadius * 2;
return { imageSize, canvasSize }; return { imageSize, canvasSize };
}, [blurRadius, margin]); }, [blurRadius, margin]);
const skiaImage = useMemo(() => (image || fallback), [image, fallback]);
return ( return (
<Container size={imageSize} style={style}> <Container size={imageSize} style={style}>
<BlurContainer size={canvasSize} offset={blurRadius}> <BlurContainer size={canvasSize} offset={blurRadius}>
@@ -63,18 +67,16 @@ function CoverImage({
<Shadow dx={0} dy={8} blur={16} color="#0000000d" /> <Shadow dx={0} dy={8} blur={16} color="#0000000d" />
<Shadow dx={0} dy={16} blur={32} color="#0000000d" /> <Shadow dx={0} dy={16} blur={32} color="#0000000d" />
</RoundedRect> </RoundedRect>
{image ? ( {skiaImage ? (
<> <>
<SkiaImage image={image} width={imageSize} height={imageSize} opacity={opacity}> <SkiaImage image={skiaImage} width={imageSize} height={imageSize} opacity={opacity}>
<Offset x={blurRadius} y={blurRadius} /> <Offset x={blurRadius} y={blurRadius} />
<Blur blur={blurRadius / 2} /> <Blur blur={blurRadius / 2} />
</SkiaImage> </SkiaImage>
<Mask mask={<RoundedRect width={imageSize} height={imageSize} x={blurRadius} y={blurRadius} r={radius} />}> <Mask mask={<RoundedRect width={imageSize} height={imageSize} x={blurRadius} y={blurRadius} r={radius} />}>
{image ? ( <SkiaImage image={skiaImage} width={imageSize} height={imageSize}>
<SkiaImage image={image} width={imageSize} height={imageSize}> <Offset x={blurRadius} y={blurRadius} />
<Offset x={blurRadius} y={blurRadius} /> </SkiaImage>
</SkiaImage>
) : null}
</Mask> </Mask>
</> </>
) : null} ) : null}

View File

@@ -2,23 +2,35 @@ import React, { useCallback, useRef } from 'react';
import { Platform, TextInput, TextInputProps } from 'react-native'; import { Platform, TextInput, TextInputProps } from 'react-native';
import styled, { css } from 'styled-components/native'; import styled, { css } from 'styled-components/native';
import useDefaultStyles from './Colors'; import useDefaultStyles from './Colors';
import { Gap } from './Utility';
export interface InputProps extends TextInputProps { export interface InputProps extends TextInputProps {
icon?: React.ReactNode; icon?: React.ReactNode;
} }
const Container = styled.Pressable` const Container = styled.Pressable<{ hasIcon?: boolean }>`
position: relative;
margin: 6px 0; margin: 6px 0;
border-radius: 8px; border-radius: 12px;
display: flex; display: flex;
flex-direction: row;
align-items: center;
${Platform.select({ ${Platform.select({
ios: css`padding: 12px;`, ios: css`padding: 12px;`,
android: css`padding: 4px 12px;`, android: css`padding: 4px 12px;`,
})} })}
${({ hasIcon }) => hasIcon && css`
padding-left: 36px;
`}
`;
const IconWrapper = styled.View`
position: absolute;
left: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
padding-left: 12px;
`; `;
function Input({ icon = null, style, testID, ...rest }: InputProps) { function Input({ icon = null, style, testID, ...rest }: InputProps) {
@@ -28,12 +40,17 @@ function Input({ icon = null, style, testID, ...rest }: InputProps) {
const handlePress = useCallback(() => inputRef.current?.focus(), []); const handlePress = useCallback(() => inputRef.current?.focus(), []);
return ( return (
<Container style={[defaultStyles.input, style]} onPress={handlePress} testID={`${testID}-container`} accessible={false}> <Container
style={[defaultStyles.input, style]}
onPress={handlePress}
testID={`${testID}-container`}
accessible={false}
hasIcon={!!icon}
>
{icon && ( {icon && (
<> <IconWrapper>
{icon} {icon}
<Gap size={8} /> </IconWrapper>
</>
)} )}
<TextInput <TextInput
{...rest} {...rest}

View File

@@ -14,7 +14,6 @@ const Background = styled.View`
const Container = styled.View<Pick<Props, 'fullSize'>>` const Container = styled.View<Pick<Props, 'fullSize'>>`
margin: auto 20px; margin: auto 20px;
padding: 4px;
border-radius: 12px; border-radius: 12px;
flex: 0 0 auto; flex: 0 0 auto;
background: salmon; background: salmon;

View File

@@ -0,0 +1,100 @@
import React, { ForwardedRef, Ref, forwardRef } from 'react';
import { useHeaderHeight } from '@react-navigation/elements';
import { FlatList, FlatListProps, ScrollView, ScrollViewProps, SectionList, SectionListProps } from 'react-native';
import useCurrentTrack from '../utility/useCurrentTrack';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
declare module 'react' {
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
/**
* A wrapper for ScrollView that takes any paddings, margins and insets into
* account that result from the bottom tabs, potential NowPlaying overlay and header.
*/
export function SafeScrollView({
contentContainerStyle,
...props
}: ScrollViewProps) {
const { top, bottom } = useNavigationOffsets();
return (
<ScrollView
contentContainerStyle={[
contentContainerStyle,
{ paddingTop: top, paddingBottom: bottom },
]}
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
{...props}
/>
);
}
/**
* A wrapper for ScrollView that takes any paddings, margins and insets into
* account that result from the bottom tabs, potential NowPlaying overlay and header.
*/
function BareSafeSectionList<I, S>({
contentContainerStyle,
...props
}: SectionListProps<I, S>, ref: ForwardedRef<SectionList<I, S>>) {
const { top, bottom } = useNavigationOffsets();
return (
<SectionList
contentContainerStyle={[
{ paddingTop: top, paddingBottom: bottom },
contentContainerStyle,
]}
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
ref={ref}
{...props}
/>
);
}
export const SafeSectionList = forwardRef(BareSafeSectionList);
/**
* A wrapper for ScrollView that takes any paddings, margins and insets into
* account that result from the bottom tabs, potential NowPlaying overlay and header.
*/
function BareSafeFlatList<I>({
contentContainerStyle,
...props
}: FlatListProps<I>, ref: ForwardedRef<FlatList<I>>) {
const { top, bottom } = useNavigationOffsets();
return (
<FlatList
contentContainerStyle={[
{ paddingTop: top, paddingBottom: bottom },
contentContainerStyle,
]}
scrollIndicatorInsets={{ top, bottom }}
ref={ref}
{...props}
/>
);
}
export const SafeFlatList = forwardRef(BareSafeFlatList);
/**
* A hook that returns the correct offset that should be applied to any Views
* that are wrapped in a NavigationView, in order to account for overlays,
* headers and bottom tabs.
*/
export function useNavigationOffsets({ includeOverlay = true } = {} as { includeOverlay?: boolean }) {
const headerHeight = useHeaderHeight();
const bottomBarHeight = useBottomTabBarHeight();
const { track } = useCurrentTrack();
return {
top: headerHeight,
bottom: (track && includeOverlay ? 68 : 0) + bottomBarHeight || 0,
};
}

View File

@@ -15,7 +15,8 @@ export function Text(props: PropsWithChildren<TextProps>) {
export const Header = styled(Text)` export const Header = styled(Text)`
margin: 0 0 6px 0; margin: 0 0 6px 0;
font-size: 28px; font-size: 28px;
font-weight: 400; font-weight: 500;
letter-spacing: -0.3px;
`; `;
export const SubHeader = styled(Text)` export const SubHeader = styled(Text)`
@@ -24,3 +25,9 @@ export const SubHeader = styled(Text)`
font-weight: 400; font-weight: 400;
opacity: 0.5; opacity: 0.5;
`; `;
export const Paragraph = styled(Text)`
opacity: 0.5;
font-size: 12px;
line-height: 20px;
`;

View File

@@ -59,5 +59,7 @@
"you-are-offline-message": "You are currently offline. You can only play previously downloaded music.", "you-are-offline-message": "You are currently offline. You can only play previously downloaded music.",
"playing-on": "Playing on", "playing-on": "Playing on",
"local-playback": "Local playback", "local-playback": "Local playback",
"streaming": "Streaming" "streaming": "Streaming",
"total-duration": "Total duration",
"similar-albums": "Similar albums"
} }

View File

@@ -59,5 +59,7 @@
"you-are-offline-message": "Je bent op dit moment offline. Je kunt alleen eerder gedownloade nummers afspelen.", "you-are-offline-message": "Je bent op dit moment offline. Je kunt alleen eerder gedownloade nummers afspelen.",
"playing-on": "Speelt af op", "playing-on": "Speelt af op",
"local-playback": "Lokaal afspelen", "local-playback": "Lokaal afspelen",
"streaming": "Streamen" "streaming": "Streamen",
"total-duration": "Totale duur",
"similar-albums": "Vergelijkbare albums"
} }

View File

@@ -57,4 +57,6 @@ export type LocaleKeys = 'play-next'
| 'you-are-offline-message' | 'you-are-offline-message'
| 'playing-on' | 'playing-on'
| 'local-playback' | 'local-playback'
| 'streaming' | 'streaming'
| 'total-duration'
| 'similar-albums'

View File

@@ -1,7 +1,6 @@
import useDefaultStyles from 'components/Colors'; import useDefaultStyles from 'components/Colors';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { FlatListProps, View } from 'react-native'; import { FlatListProps, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
import formatBytes from 'utility/formatBytes'; import formatBytes from 'utility/formatBytes';
@@ -17,6 +16,7 @@ import { Text } from 'components/Typography';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import { ShadowWrapper } from 'components/Shadow'; import { ShadowWrapper } from 'components/Shadow';
import { SafeFlatList } from 'components/SafeNavigatorView';
const DownloadedTrack = styled.View` const DownloadedTrack = styled.View`
flex: 1 0 auto; flex: 1 0 auto;
@@ -82,7 +82,7 @@ function Downloads() {
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{t('total-download-size')}: {formatBytes(totalDownloadSize)} {t('total-download-size')}{': '}{formatBytes(totalDownloadSize)}
</Text> </Text>
<Button <Button
icon={TrashIcon} icon={TrashIcon}
@@ -151,10 +151,10 @@ function Downloads() {
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
{ListHeaderComponent} {ListHeaderComponent}
<FlatList <SafeFlatList
data={ids} data={ids}
style={{ flex: 1, paddingTop: 12 }} style={{ flex: 1, paddingTop: 12 }}
contentContainerStyle={{ flexGrow: 1, paddingBottom: 24 }} contentContainerStyle={{ flexGrow: 1 }}
renderItem={renderItem} renderItem={renderItem}
/> />
</SafeAreaView> </SafeAreaView>

View File

@@ -1,18 +1,20 @@
import React from 'react'; import React from 'react';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MusicStackParams } from './types';
import Albums from './stacks/Albums';
import Album from './stacks/Album';
import RecentAlbums from './stacks/RecentAlbums';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import { t } from '@localisation'; import { t } from '@localisation';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import Playlists from './stacks/Playlists'; import { StackParams } from 'screens/types';
import Playlist from './stacks/Playlist';
import NowPlaying from './overlays/NowPlaying'; import NowPlaying from './overlays/NowPlaying';
const Stack = createStackNavigator<MusicStackParams>(); import RecentAlbums from './stacks/RecentAlbums';
import Albums from './stacks/Albums';
import Album from './stacks/Album';
import Playlists from './stacks/Playlists';
import Playlist from './stacks/Playlist';
import { StyleSheet } from 'react-native';
const Stack = createStackNavigator<StackParams>();
function MusicStack() { function MusicStack() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
@@ -23,8 +25,10 @@ function MusicStack() {
headerTintColor: THEME_COLOR, headerTintColor: THEME_COLOR,
headerTitleStyle: defaultStyles.stackHeader, headerTitleStyle: defaultStyles.stackHeader,
cardStyle: defaultStyles.view, cardStyle: defaultStyles.view,
headerTransparent: true,
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
}}> }}>
<Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums') }} /> <Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums'), headerShown: false }} />
<Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} /> <Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} />
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} /> <Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
<Stack.Screen name="Playlists" component={Playlists} options={{ headerTitle: t('playlists') }} /> <Stack.Screen name="Playlists" component={Playlists} options={{ headerTitle: t('playlists') }} />

View File

@@ -15,15 +15,15 @@ import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { calculateProgressTranslation } from 'components/Progresstrack'; import { calculateProgressTranslation } from 'components/Progresstrack';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import { MusicNavigationProp } from 'screens/Music/types'; import { NavigationProp } from 'screens/types';
import { ShadowWrapper } from 'components/Shadow'; import { ShadowWrapper } from 'components/Shadow';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
const NOW_PLAYING_POPOVER_MARGIN = 6; const NOW_PLAYING_POPOVER_MARGIN = 6;
const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN; const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN;
const PopoverPosition = css` const PopoverPosition = css`
position: absolute; position: absolute;
bottom: ${NOW_PLAYING_POPOVER_MARGIN}px;
left: ${NOW_PLAYING_POPOVER_MARGIN}px; left: ${NOW_PLAYING_POPOVER_MARGIN}px;
right: ${NOW_PLAYING_POPOVER_MARGIN}px; right: ${NOW_PLAYING_POPOVER_MARGIN}px;
border-radius: 8px; border-radius: 8px;
@@ -111,10 +111,11 @@ function NowPlaying() {
const { index, track } = useCurrentTrack(); const { index, track } = useCurrentTrack();
const { buffered, position } = useProgress(); const { buffered, position } = useProgress();
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const tabBarHeight = useBottomTabBarHeight();
const previousBuffered = usePrevious(buffered); const previousBuffered = usePrevious(buffered);
const previousPosition = usePrevious(position); const previousPosition = usePrevious(position);
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const bufferAnimation = useRef(new Animated.Value(0)); const bufferAnimation = useRef(new Animated.Value(0));
const progressAnimation = useRef(new Animated.Value(0)); const progressAnimation = useRef(new Animated.Value(0));
@@ -164,7 +165,7 @@ function NowPlaying() {
} }
return ( return (
<Container> <Container style={{ bottom: tabBarHeight + NOW_PLAYING_POPOVER_MARGIN }}>
{/** TODO: Fix shadow overflow on Android */} {/** TODO: Fix shadow overflow on Android */}
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
<ShadowOverlay pointerEvents='none'> <ShadowOverlay pointerEvents='none'>

View File

@@ -1,14 +1,54 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { MusicStackParams } from '../types'; import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
import { useRoute, RouteProp } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
import TrackListView from './components/TrackListView'; import TrackListView from './components/TrackListView';
import { fetchTracksByAlbum } from 'store/music/actions'; import { fetchAlbum, fetchTracksByAlbum } from 'store/music/actions';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS'; import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
import { t } from '@localisation'; import { t } from '@localisation';
import { NavigationProp, StackParams } from 'screens/types';
import { SubHeader, Text } from 'components/Typography';
import { ScrollView } from 'react-native-gesture-handler';
import { useGetImage } from 'utility/JellyfinApi';
import styled from 'styled-components';
import { Dimensions, Pressable } from 'react-native';
import AlbumImage from './components/AlbumImage';
type Route = RouteProp<MusicStackParams, 'Album'>; type Route = RouteProp<StackParams, 'Album'>;
const Screen = Dimensions.get('screen');
const Cover = styled(AlbumImage)`
height: ${Screen.width / 2.8};
width: ${Screen.width / 2.8};
border-radius: 12px;
margin-bottom: 8px;
`;
function SimilarAlbum({ id }: { id: string }) {
const navigation = useNavigation<NavigationProp>();
const getImage = useGetImage();
const album = useTypedSelector((state) => state.music.albums.entities[id]);
const handlePress = useCallback(() => {
album && navigation.push('Album', { id, album });
}, [id, album, navigation]);
return (
<Pressable
style={({ pressed }) => ({
opacity: pressed ? 0.5 : 1.0,
width: Screen.width / 2.8,
marginRight: 12
})}
onPress={handlePress}
>
<Cover key={id} source={{ uri: getImage(id) }} />
<Text numberOfLines={1} style={{ fontSize: 13, marginBottom: 2 }}>{album?.Name}</Text>
<Text numberOfLines={1} style={{ opacity: 0.5, fontSize: 13 }}>{album?.Artists.join(', ')}</Text>
</Pressable>
);
}
const Album: React.FC = () => { const Album: React.FC = () => {
const { params: { id } } = useRoute<Route>(); const { params: { id } } = useRoute<Route>();
@@ -19,7 +59,10 @@ const Album: React.FC = () => {
const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]); const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]);
// Define a function for refreshing this entity // Define a function for refreshing this entity
const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); }, [id, dispatch]); const refresh = useCallback(() => {
dispatch(fetchTracksByAlbum(id));
dispatch(fetchAlbum(id));
}, [id, dispatch]);
// Auto-fetch the track data periodically // Auto-fetch the track data periodically
useEffect(() => { useEffect(() => {
@@ -39,7 +82,21 @@ const Album: React.FC = () => {
shuffleButtonText={t('shuffle-album')} shuffleButtonText={t('shuffle-album')}
downloadButtonText={t('download-album')} downloadButtonText={t('download-album')}
deleteButtonText={t('delete-album')} deleteButtonText={t('delete-album')}
/> >
{album?.Overview ? (
<Text style={{ opacity: 0.5, lineHeight: 20, fontSize: 12, paddingBottom: 24 }}>{album?.Overview}</Text>
) : null}
{album?.Similar?.length ? (
<>
<SubHeader>{t('similar-albums')}</SubHeader>
<ScrollView horizontal style={{ marginLeft: -24, marginRight: -24, marginTop: 8 }} contentContainerStyle={{ paddingHorizontal: 24 }} showsHorizontalScrollIndicator={false}>
{album.Similar.map((id) => (
<SimilarAlbum id={id} key={id} />
))}
</ScrollView>
</>
) : null}
</TrackListView>
); );
}; };

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react'; import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import { MusicNavigationProp } from '../types'; import { SectionList, View } from 'react-native';
import { SafeAreaView, SectionList, View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
@@ -17,6 +16,8 @@ import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { Album } from 'store/music/types'; import { Album } from 'store/music/types';
import { Text } from 'components/Typography'; import { Text } from 'components/Typography';
import { ShadowWrapper } from 'components/Shadow'; import { ShadowWrapper } from 'components/Shadow';
import { NavigationProp } from 'screens/types';
import { SafeSectionList } from 'components/SafeNavigatorView';
const HeadingHeight = 50; const HeadingHeight = 50;
@@ -87,7 +88,7 @@ const Albums: React.FC = () => {
// Initialise helpers // Initialise helpers
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const getImage = useGetImage(); const getImage = useGetImage();
const listRef = useRef<SectionList<EntityId[]>>(null); const listRef = useRef<SectionList<EntityId[]>>(null);
@@ -168,9 +169,9 @@ const Albums: React.FC = () => {
}); });
return ( return (
<SafeAreaView> <>
<AlphabetScroller onSelect={selectLetter} /> <AlphabetScroller onSelect={selectLetter} />
<SectionList <SafeSectionList
sections={sections} sections={sections}
refreshing={isLoading} refreshing={isLoading}
onRefresh={retrieveData} onRefresh={retrieveData}
@@ -180,7 +181,7 @@ const Albums: React.FC = () => {
renderSectionHeader={generateSection} renderSectionHeader={generateSection}
renderItem={generateItem} renderItem={generateItem}
/> />
</SafeAreaView> </>
); );
}; };

View File

@@ -1,5 +1,4 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { MusicStackParams } from '../types';
import { useRoute, RouteProp } from '@react-navigation/native'; import { useRoute, RouteProp } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
import TrackListView from './components/TrackListView'; import TrackListView from './components/TrackListView';
@@ -7,8 +6,9 @@ import { fetchTracksByPlaylist } from 'store/music/actions';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS'; import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
import { t } from '@localisation'; import { t } from '@localisation';
import { StackParams } from 'screens/types';
type Route = RouteProp<MusicStackParams, 'Album'>; type Route = RouteProp<StackParams, 'Album'>;
const Playlist: React.FC = () => { const Playlist: React.FC = () => {
const { params: { id } } = useRoute<Route>(); const { params: { id } } = useRoute<Route>();
@@ -39,6 +39,7 @@ const Playlist: React.FC = () => {
shuffleButtonText={t('shuffle-playlist')} shuffleButtonText={t('shuffle-playlist')}
downloadButtonText={t('download-playlist')} downloadButtonText={t('download-playlist')}
deleteButtonText={t('delete-playlist')} deleteButtonText={t('delete-playlist')}
itemDisplayStyle='playlist'
/> />
); );
}; };

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useRef, ReactText } from 'react'; import React, { useCallback, useEffect, useRef, ReactText } from 'react';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import { MusicNavigationProp } from '../types'; import { Text, View, FlatList, ListRenderItem, RefreshControl } from 'react-native';
import { Text, View, FlatList, ListRenderItem } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
@@ -11,6 +10,8 @@ import TouchableHandler from 'components/TouchableHandler';
import AlbumImage, { AlbumItem } from './components/AlbumImage'; import AlbumImage, { AlbumItem } from './components/AlbumImage';
import { EntityId } from '@reduxjs/toolkit'; import { EntityId } from '@reduxjs/toolkit';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles from 'components/Colors';
import { NavigationProp } from 'screens/types';
import { SafeFlatList, useNavigationOffsets } from 'components/SafeNavigatorView';
interface GeneratedAlbumItemProps { interface GeneratedAlbumItemProps {
id: ReactText; id: ReactText;
@@ -34,6 +35,8 @@ const GeneratedPlaylistItem = React.memo(function GeneratedPlaylistItem(props: G
}); });
const Playlists: React.FC = () => { const Playlists: React.FC = () => {
const offsets = useNavigationOffsets();
// Retrieve data from store // Retrieve data from store
const { entities, ids } = useTypedSelector((state) => state.music.playlists); const { entities, ids } = useTypedSelector((state) => state.music.playlists);
const isLoading = useTypedSelector((state) => state.music.playlists.isLoading); const isLoading = useTypedSelector((state) => state.music.playlists.isLoading);
@@ -41,7 +44,7 @@ const Playlists: React.FC = () => {
// Initialise helpers // Initialise helpers
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const getImage = useGetImage(); const getImage = useGetImage();
const listRef = useRef<FlatList<EntityId>>(null); const listRef = useRef<FlatList<EntityId>>(null);
@@ -93,10 +96,11 @@ const Playlists: React.FC = () => {
}); });
return ( return (
<FlatList <SafeFlatList
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={retrieveData} progressViewOffset={offsets.top} />
}
data={ids} data={ids}
refreshing={isLoading}
onRefresh={retrieveData}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
ref={listRef} ref={listRef}
keyExtractor={(item, index) => `${item}_${index}`} keyExtractor={(item, index) => `${item}_${index}`}

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import { MusicNavigationProp } from '../types'; import { Text, SafeAreaView, StyleSheet } from 'react-native';
import { Text, SafeAreaView, FlatList, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
import { fetchRecentAlbums } from 'store/music/actions'; import { fetchRecentAlbums } from 'store/music/actions';
@@ -17,6 +16,8 @@ import { Album } from 'store/music/types';
import Divider from 'components/Divider'; import Divider from 'components/Divider';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { ShadowWrapper } from 'components/Shadow'; import { ShadowWrapper } from 'components/Shadow';
import { NavigationProp } from 'screens/types';
import { SafeFlatList } from 'components/SafeNavigatorView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
columnWrapper: { columnWrapper: {
@@ -31,7 +32,7 @@ const HeaderContainer = styled.View`
`; `;
const NavigationHeader: React.FC = () => { const NavigationHeader: React.FC = () => {
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]); const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]); const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
@@ -59,7 +60,7 @@ const RecentAlbums: React.FC = () => {
// Initialise helpers // Initialise helpers
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const getImage = useGetImage(); const getImage = useGetImage();
// Set callbacks // Set callbacks
@@ -71,7 +72,7 @@ const RecentAlbums: React.FC = () => {
return ( return (
<SafeAreaView> <SafeAreaView>
<FlatList <SafeFlatList
data={recentAlbums as string[]} data={recentAlbums as string[]}
refreshing={isLoading} refreshing={isLoading}
onRefresh={retrieveData} onRefresh={retrieveData}

View File

@@ -1,23 +0,0 @@
import styled from 'styled-components/native';
import FastImage from 'react-native-fast-image';
import { Dimensions } from 'react-native';
const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24;
export const AlbumHeight = AlbumWidth + 40;
export const CoverSize = AlbumWidth - 16;
export const AlbumItem = styled.View`
width: ${AlbumWidth}px;
height: ${AlbumHeight}px;
padding: 8px;
`;
const AlbumImage = styled(FastImage)`
border-radius: 10px;
width: ${CoverSize}px;
height: ${CoverSize}px;
margin-bottom: 5px;
`;
export default AlbumImage;

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import styled from 'styled-components/native';
import FastImage, { FastImageProps } from 'react-native-fast-image';
import { Dimensions, useColorScheme } from 'react-native';
const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24;
export const AlbumHeight = AlbumWidth + 40;
export const CoverSize = AlbumWidth - 16;
export const AlbumItem = styled.View`
width: ${AlbumWidth}px;
height: ${AlbumHeight}px;
padding: 8px;
`;
const Container = styled(FastImage)`
border-radius: 10px;
width: ${CoverSize}px;
height: ${CoverSize}px;
margin-bottom: 5px;
`;
function AlbumImage(props: FastImageProps) {
const [hasError, setError] = useState(false);
const colorScheme = useColorScheme();
if (!props.source || hasError) {
return (
<Container {...props} source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
);
}
return (
<Container {...props} onError={() => setError(true)} />
);
}
export default AlbumImage;

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react'; import React, { PropsWithChildren, useCallback, useMemo } from 'react';
import { ScrollView, RefreshControl, StyleSheet, View } from 'react-native'; import { Platform, RefreshControl, StyleSheet, View } from 'react-native';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import styled, { css } from 'styled-components/native'; import styled, { css } from 'styled-components/native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
@@ -14,7 +14,7 @@ import useDefaultStyles from 'components/Colors';
import usePlayTracks from 'utility/usePlayTracks'; import usePlayTracks from 'utility/usePlayTracks';
import { EntityId } from '@reduxjs/toolkit'; import { EntityId } from '@reduxjs/toolkit';
import { WrappableButtonRow, WrappableButton } from 'components/WrappableButtonRow'; import { WrappableButtonRow, WrappableButton } from 'components/WrappableButtonRow';
import { MusicNavigationProp } from 'screens/Music/types'; import { NavigationProp } from 'screens/types';
import DownloadIcon from 'components/DownloadIcon'; import DownloadIcon from 'components/DownloadIcon';
import CloudDownArrow from 'assets/icons/cloud-down-arrow.svg'; import CloudDownArrow from 'assets/icons/cloud-down-arrow.svg';
import Trash from 'assets/icons/trash.svg'; import Trash from 'assets/icons/trash.svg';
@@ -25,6 +25,8 @@ import { Text } from 'components/Typography';
import CoverImage from 'components/CoverImage'; import CoverImage from 'components/CoverImage';
import ticksToDuration from 'utility/ticksToDuration'; import ticksToDuration from 'utility/ticksToDuration';
import { t } from '@localisation';
import { SafeScrollView, useNavigationOffsets } from 'components/SafeNavigatorView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
index: { index: {
@@ -42,18 +44,23 @@ const AlbumImageContainer = styled.View`
align-items: center; align-items: center;
`; `;
const TrackContainer = styled.View<{ isPlaying: boolean }>` const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>`
padding: 12px 4px; padding: 12px 4px;
flex-direction: row; flex-direction: row;
border-radius: 6px; border-radius: 6px;
align-items: flex-start;
${props => props.isPlaying && css` ${props => props.isPlaying && css`
margin: 0 -12px; margin: 0 -12px;
padding: 12px 16px; padding: 12px 16px;
`} `}
${props => props.small && css`
padding: ${Platform.select({ ios: '8px 4px', android: '4px'})};
`}
`; `;
interface TrackListViewProps { export interface TrackListViewProps extends PropsWithChildren<{}> {
title?: string; title?: string;
artist?: string; artist?: string;
trackIds: EntityId[]; trackIds: EntityId[];
@@ -64,6 +71,7 @@ interface TrackListViewProps {
downloadButtonText: string; downloadButtonText: string;
deleteButtonText: string; deleteButtonText: string;
listNumberingStyle?: 'album' | 'index'; listNumberingStyle?: 'album' | 'index';
itemDisplayStyle?: 'album' | 'playlist';
} }
const TrackListView: React.FC<TrackListViewProps> = ({ const TrackListView: React.FC<TrackListViewProps> = ({
@@ -77,19 +85,27 @@ const TrackListView: React.FC<TrackListViewProps> = ({
downloadButtonText, downloadButtonText,
deleteButtonText, deleteButtonText,
listNumberingStyle = 'album', listNumberingStyle = 'album',
itemDisplayStyle = 'album',
children
}) => { }) => {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const offsets = useNavigationOffsets();
// Retrieve state // Retrieve state
const tracks = useTypedSelector((state) => state.music.tracks.entities); const tracks = useTypedSelector((state) => state.music.tracks.entities);
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading); const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds)); const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
const totalDuration = useMemo(() => (
trackIds.reduce<number>((sum, trackId) => (
sum + (tracks[trackId]?.RunTimeTicks || 0)
), 0)
), [trackIds, tracks]);
// Retrieve helpers // Retrieve helpers
const getImage = useGetImage(); const getImage = useGetImage();
const playTracks = usePlayTracks(); const playTracks = usePlayTracks();
const { track: currentTrack } = useCurrentTrack(); const { track: currentTrack } = useCurrentTrack();
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// Setup callbacks // Setup callbacks
@@ -101,7 +117,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
await TrackPlayer.play(); await TrackPlayer.play();
}, [playTracks, trackIds]); }, [playTracks, trackIds]);
const longPressTrack = useCallback((index: number) => { const longPressTrack = useCallback((index: number) => {
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index] }); navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
}, [navigation, trackIds]); }, [navigation, trackIds]);
const downloadAllTracks = useCallback(() => { const downloadAllTracks = useCallback(() => {
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId))); trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
@@ -111,90 +127,109 @@ const TrackListView: React.FC<TrackListViewProps> = ({
}, [dispatch, downloadedTracks]); }, [dispatch, downloadedTracks]);
return ( return (
<ScrollView <SafeScrollView
style={defaultStyles.view} style={defaultStyles.view}
contentContainerStyle={{ padding: 24, paddingTop: 32, paddingBottom: 64 }}
refreshControl={ refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refresh} /> <RefreshControl refreshing={isLoading} onRefresh={refresh} progressViewOffset={offsets.top} />
} }
> >
<AlbumImageContainer> <View style={{ padding: 24, paddingTop: 32, paddingBottom: 32 }}>
<CoverImage src={getImage(entityId)} /> <AlbumImageContainer>
</AlbumImageContainer> <CoverImage src={getImage(entityId)} />
<Header>{title}</Header> </AlbumImageContainer>
<SubHeader>{artist}</SubHeader> <Header>{title}</Header>
<WrappableButtonRow> <SubHeader>{artist}</SubHeader>
<WrappableButton title={playButtonText} icon={Play} onPress={playEntity} testID="play-album" /> <WrappableButtonRow>
<WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" /> <WrappableButton title={playButtonText} icon={Play} onPress={playEntity} testID="play-album" />
</WrappableButtonRow> <WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" />
<View style={{ marginTop: 8 }}> </WrappableButtonRow>
{trackIds.map((trackId, i) => <View style={{ marginTop: 8 }}>
<TouchableHandler {trackIds.map((trackId, i) =>
key={trackId} <TouchableHandler
id={i} key={trackId}
onPress={selectTrack} id={i}
onLongPress={longPressTrack} onPress={selectTrack}
testID={`play-track-${trackId}`} onLongPress={longPressTrack}
> testID={`play-track-${trackId}`}
<TrackContainer
isPlaying={currentTrack?.backendId === trackId || false}
style={[defaultStyles.border, currentTrack?.backendId === trackId || false ? defaultStyles.activeBackground : null ]}
> >
<Text <TrackContainer
style={[ isPlaying={currentTrack?.backendId === trackId || false}
styles.index, style={[defaultStyles.border, currentTrack?.backendId === trackId || false ? defaultStyles.activeBackground : null ]}
{ opacity: 0.25 }, small={itemDisplayStyle === 'playlist'}
currentTrack?.backendId === trackId && styles.activeText
]}
numberOfLines={1}
> >
{listNumberingStyle === 'index'
? i + 1
: tracks[trackId]?.IndexNumber}
</Text>
<Text
style={{
...currentTrack?.backendId === trackId && styles.activeText,
flexShrink: 1,
marginRight: 4,
}}
numberOfLines={1}
>
{tracks[trackId]?.Name}
</Text>
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}>
<Text <Text
style={[ style={[
{ marginRight: 12, opacity: 0.25 }, styles.index,
{ opacity: 0.25 },
currentTrack?.backendId === trackId && styles.activeText currentTrack?.backendId === trackId && styles.activeText
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} {listNumberingStyle === 'index'
? i + 1
: tracks[trackId]?.IndexNumber}
</Text> </Text>
<DownloadIcon trackId={trackId} fill={currentTrack?.backendId === trackId ? `${THEME_COLOR}44` : undefined} /> <View style={{ flexShrink: 1 }}>
</View> <Text
</TrackContainer> style={{
</TouchableHandler> ...currentTrack?.backendId === trackId && styles.activeText,
)} flexShrink: 1,
<WrappableButtonRow style={{ marginTop: 24 }}> marginRight: 4,
<WrappableButton }}
icon={CloudDownArrow} numberOfLines={1}
title={downloadButtonText} >
onPress={downloadAllTracks} {tracks[trackId]?.Name}
disabled={downloadedTracks.length === trackIds.length} </Text>
testID="download-album" {itemDisplayStyle === 'playlist' && (
/> <Text
<WrappableButton style={{
icon={Trash} ...currentTrack?.backendId === trackId && styles.activeText,
title={deleteButtonText} flexShrink: 1,
onPress={deleteAllTracks} marginRight: 4,
disabled={downloadedTracks.length === 0} opacity:currentTrack?.backendId === trackId ? 0.5 : 0.25,
testID="delete-album" }}
/> numberOfLines={1}
</WrappableButtonRow> >
{tracks[trackId]?.Artists.join(', ')}
</Text>
)}
</View>
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}>
<Text
style={[
{ marginRight: 12, opacity: 0.25 },
currentTrack?.backendId === trackId && styles.activeText
]}
numberOfLines={1}
>
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)}
</Text>
<DownloadIcon trackId={trackId} fill={currentTrack?.backendId === trackId ? `${THEME_COLOR}44` : undefined} />
</View>
</TrackContainer>
</TouchableHandler>
)}
<Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>{t('total-duration')}{': '}{ticksToDuration(totalDuration)}</Text>
<WrappableButtonRow style={{ marginTop: 24 }}>
<WrappableButton
icon={CloudDownArrow}
title={downloadButtonText}
onPress={downloadAllTracks}
disabled={downloadedTracks.length === trackIds.length}
testID="download-album"
/>
<WrappableButton
icon={Trash}
title={deleteButtonText}
onPress={deleteAllTracks}
disabled={downloadedTracks.length === 0}
testID="delete-album"
/>
</WrappableButtonRow>
</View>
{children}
</View> </View>
</ScrollView> </SafeScrollView>
); );
}; };

View File

@@ -1,328 +1,31 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React from 'react';
import Input from 'components/Input'; import { createStackNavigator } from '@react-navigation/stack';
import { ActivityIndicator, Animated, SafeAreaView, View } from 'react-native'; import { THEME_COLOR } from 'CONSTANTS';
import styled from 'styled-components/native';
import { useAppDispatch, useTypedSelector } from 'store';
import Fuse from 'fuse.js';
import { Album, AlbumTrack } from 'store/music/types';
import { FlatList } from 'react-native-gesture-handler';
import TouchableHandler from 'components/TouchableHandler';
import { useNavigation } from '@react-navigation/native';
import { useGetImage } from 'utility/JellyfinApi';
import FastImage from 'react-native-fast-image';
import { t } from '@localisation'; import { t } from '@localisation';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { searchAndFetchAlbums } from 'store/music/actions'; import { StackParams } from 'screens/types';
import { debounce } from 'lodash'; import Search from './stacks/Search';
import { Text } from 'components/Typography'; import Album from 'screens/Music/stacks/Album';
import { MusicNavigationProp } from 'screens/Music/types'; import { StyleSheet } from 'react-native';
import DownloadIcon from 'components/DownloadIcon';
import ChevronRight from 'assets/icons/chevron-right.svg';
import SearchIcon from 'assets/icons/magnifying-glass.svg';
import { ShadowWrapper } from 'components/Shadow';
import { useKeyboardHeight } from 'utility/useKeyboardHeight';
// import MicrophoneIcon from 'assets/icons/microphone.svg';
// import AlbumIcon from 'assets/icons/collection.svg';
// import TrackIcon from 'assets/icons/note.svg';
// import PlaylistIcon from 'assets/icons/note-list.svg';
// import StreamIcon from 'assets/icons/cloud.svg';
// import LocalIcon from 'assets/icons/internal-drive.svg';
// import SelectableFilter from './components/SelectableFilter';
const Container = styled(Animated.View)` const Stack = createStackNavigator<StackParams>();
padding: 4px 32px 0 32px;
margin-bottom: 0px;
padding-bottom: 0px;
border-top-width: 0.5px;
`;
const FullSizeContainer = styled.View` function SearchStack() {
flex: 1;
`;
const Loading = styled.View`
position: absolute;
right: 12px;
top: 0;
height: 100%;
flex: 1;
justify-content: center;
`;
const AlbumImage = styled(FastImage)`
border-radius: 4px;
width: 32px;
height: 32px;
margin-right: 10px;
`;
const HalfOpacity = styled.Text`
opacity: 0.5;
margin-top: 2px;
font-size: 12px;
flex: 1 1 auto;
`;
const SearchResult = styled.View`
flex-direction: row;
align-items: center;
padding: 8px 32px;
height: 54px;
`;
const fuseOptions: Fuse.IFuseOptions<Album> = {
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
threshold: 0.1,
includeScore: true,
fieldNormWeight: 1,
};
type AudioResult = {
type: 'Audio',
id: string;
album: string;
name: string;
};
type AlbumResult = {
type: 'AlbumArtist',
id: string;
album: undefined;
name: undefined;
}
type CombinedResults = (AudioResult | AlbumResult)[];
export default function Search() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
// Prepare state for fuse and albums
const [fuseIsReady, setFuseReady] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setLoading] = useState(false);
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
const albums = useTypedSelector(state => state.music.albums.entities);
const fuse = useRef<Fuse<Album>>();
// Prepare helpers
const navigation = useNavigation<MusicNavigationProp>();
const keyboardHeight = useKeyboardHeight();
const getImage = useGetImage();
const dispatch = useAppDispatch();
/**
* Since it is impractical to have a global fuse variable, we need to
* instantiate it for thsi function. With this effect, we generate a new
* Fuse instance every time the albums change. This can of course be done
* more intelligently by removing and adding the changed albums, but this is
* an open todo.
*/
useEffect(() => {
fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
setFuseReady(true);
}, [albums, setFuseReady]);
/**
* This function retrieves search results from Jellyfin. It is a seperate
* callback, so that we can make sure it is properly debounced and doesn't
* cause execessive jank in the interface.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
const fetchJellyfinResults = useCallback(debounce(async (searchTerm: string, currentResults: CombinedResults) => {
// First, query the Jellyfin API
const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));
// Convert the current results to album ids
const albumIds = currentResults.map(item => item.id);
// Parse the result in correct typescript form
const results = (payload as { results: (Album | AlbumTrack)[] }).results;
// Filter any results that are already displayed
const items = results.filter(item => (
!(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
// Then convert the results to proper result form
)).map((item) => ({
type: item.Type,
id: item.Id,
album: item.Type === 'Audio'
? item.AlbumId
: undefined,
name: item.Type === 'Audio'
? item.Name
: undefined,
}));
// Lastly, we'll merge the two and assign them to the state
setJellyfinResults([...items] as CombinedResults);
// Loading is now complete
setLoading(false);
}, 50), [dispatch, setJellyfinResults]);
/**
* Whenever the search term changes, we gather results from Fuse and assign
* them to state
*/
useEffect(() => {
if (!searchTerm) {
return;
}
const retrieveResults = async () => {
// GUARD: In some extraordinary cases, Fuse might not be presented since
// it is assigned via refs. In this case, we can't handle any searching.
if (!fuse.current) {
return;
}
// First set the immediate results from fuse
const fuseResults = fuse.current.search(searchTerm);
const albums: AlbumResult[] = fuseResults
.map(({ item }) => ({
id: item.Id,
type: 'AlbumArtist',
album: undefined,
name: undefined,
}));
// Assign the preliminary results
setFuseResults(albums);
setLoading(true);
try {
// Wrap the call in a try/catch block so that we catch any
// network issues in search and just use local search if the
// network is unavailable
fetchJellyfinResults(searchTerm, albums);
} catch {
// Reset the loading indicator if the network fails
setLoading(false);
}
};
retrieveResults();
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
// Handlers
const selectAlbum = useCallback((id: string) =>
navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
);
const HeaderComponent = React.useMemo(() => (
<View>
<Container style={[
defaultStyles.border,
defaultStyles.view,
{ transform: [{ translateY: keyboardHeight }]},
]}>
<View>
<Input
value={searchTerm}
onChangeText={setSearchTerm}
style={[defaultStyles.input, { marginBottom: 12 }]}
placeholder={t('search') + '...'}
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
testID="search-input"
/>
{isLoading && <Loading><ActivityIndicator /></Loading>}
</View>
</Container>
{/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
<SelectableFilter
text="Artists"
icon={MicrophoneIcon}
active
/>
<SelectableFilter
text="Albums"
icon={AlbumIcon}
active={false}
/>
<SelectableFilter
text="Tracks"
icon={TrackIcon}
active={false}
/>
<SelectableFilter
text="Playlist"
icon={PlaylistIcon}
active={false}
/>
<SelectableFilter
text="Streaming"
icon={StreamIcon}
active={false}
/>
<SelectableFilter
text="Local Playback"
icon={LocalIcon}
active={false}
/>
</View>
</ScrollView> */}
</View>
), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight]);
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
// Therefore we delay rendering to when we are certain it's there.
if (!fuseIsReady) {
return null;
}
return ( return (
<SafeAreaView style={{ flex: 1 }}> <Stack.Navigator initialRouteName="Search" screenOptions={{
<FlatList headerTintColor: THEME_COLOR,
style={{ flex: 2 }} headerTitleStyle: defaultStyles.stackHeader,
data={[...jellyfinResults, ...fuseResults]} cardStyle: defaultStyles.view,
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => { headerTransparent: true,
const album = albums[trackAlbum || id]; headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
// GUARD: If the album cannot be found in the store, we }}>
// cannot display it. <Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search'), headerShown: false }} />
if (!album) { <Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
return null; </Stack.Navigator>
}
return (
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
<SearchResult>
<ShadowWrapper>
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
<View style={{ flex: 1 }}>
<Text numberOfLines={1}>
{trackName || album.Name}
</Text>
{(album.AlbumArtist || album.Name) && (
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
{type === 'AlbumArtist'
? `${t('album')}${album.AlbumArtist}`
: `${t('track')}${album.AlbumArtist}${album.Name}`
}
</HalfOpacity>
)}
</View>
<View style={{ marginLeft: 16 }}>
<DownloadIcon trackId={id} />
</View>
<View style={{ marginLeft: 16 }}>
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
</View>
</SearchResult>
</TouchableHandler>
);
}}
keyExtractor={(item) => item.id}
extraData={[searchTerm, albums]}
/>
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
<FullSizeContainer>
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
</FullSizeContainer>
) : null}
{HeaderComponent}
</SafeAreaView>
); );
} }
export default SearchStack;

View File

@@ -0,0 +1,335 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import Input from 'components/Input';
import { ActivityIndicator, Animated, SafeAreaView, View } from 'react-native';
import styled from 'styled-components/native';
import { useAppDispatch, useTypedSelector } from 'store';
import Fuse from 'fuse.js';
import { Album, AlbumTrack } from 'store/music/types';
import { FlatList } from 'react-native-gesture-handler';
import TouchableHandler from 'components/TouchableHandler';
import { useNavigation } from '@react-navigation/native';
import { useGetImage } from 'utility/JellyfinApi';
import FastImage from 'react-native-fast-image';
import { t } from '@localisation';
import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { searchAndFetchAlbums } from 'store/music/actions';
import { debounce } from 'lodash';
import { Text } from 'components/Typography';
import DownloadIcon from 'components/DownloadIcon';
import ChevronRight from 'assets/icons/chevron-right.svg';
import SearchIcon from 'assets/icons/magnifying-glass.svg';
import { ShadowWrapper } from 'components/Shadow';
import { useKeyboardHeight } from 'utility/useKeyboardHeight';
import { NavigationProp } from 'screens/types';
import { useNavigationOffsets } from 'components/SafeNavigatorView';
// import MicrophoneIcon from 'assets/icons/microphone.svg';
// import AlbumIcon from 'assets/icons/collection.svg';
// import TrackIcon from 'assets/icons/note.svg';
// import PlaylistIcon from 'assets/icons/note-list.svg';
// import StreamIcon from 'assets/icons/cloud.svg';
// import LocalIcon from 'assets/icons/internal-drive.svg';
// import SelectableFilter from './components/SelectableFilter';
const SEARCH_INPUT_HEIGHT = 62;
const Container = styled(View)`
padding: 4px 24px 0 24px;
margin-bottom: 0px;
padding-bottom: 0px;
border-top-width: 0.5px;
`;
const FullSizeContainer = styled.View`
flex: 1;
`;
const Loading = styled.View`
position: absolute;
right: 12px;
top: 0;
height: 100%;
flex: 1;
justify-content: center;
`;
const AlbumImage = styled(FastImage)`
border-radius: 4px;
width: 32px;
height: 32px;
margin-right: 10px;
`;
const HalfOpacity = styled.Text`
opacity: 0.5;
margin-top: 2px;
font-size: 12px;
flex: 1 1 auto;
`;
const SearchResult = styled.View`
flex-direction: row;
align-items: center;
padding: 8px 32px;
height: 54px;
`;
const fuseOptions: Fuse.IFuseOptions<Album> = {
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
threshold: 0.1,
includeScore: true,
fieldNormWeight: 1,
};
type AudioResult = {
type: 'Audio',
id: string;
album: string;
name: string;
};
type AlbumResult = {
type: 'AlbumArtist',
id: string;
album: undefined;
name: undefined;
}
type CombinedResults = (AudioResult | AlbumResult)[];
export default function Search() {
const defaultStyles = useDefaultStyles();
const offsets = useNavigationOffsets({ includeOverlay: false });
// Prepare state for fuse and albums
const [fuseIsReady, setFuseReady] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setLoading] = useState(false);
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
const albums = useTypedSelector(state => state.music.albums.entities);
const fuse = useRef<Fuse<Album>>();
// Prepare helpers
const navigation = useNavigation<NavigationProp>();
const keyboardHeight = useKeyboardHeight();
const getImage = useGetImage();
const dispatch = useAppDispatch();
/**
* Since it is impractical to have a global fuse variable, we need to
* instantiate it for thsi function. With this effect, we generate a new
* Fuse instance every time the albums change. This can of course be done
* more intelligently by removing and adding the changed albums, but this is
* an open todo.
*/
useEffect(() => {
fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
setFuseReady(true);
}, [albums, setFuseReady]);
/**
* This function retrieves search results from Jellyfin. It is a seperate
* callback, so that we can make sure it is properly debounced and doesn't
* cause execessive jank in the interface.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
const fetchJellyfinResults = useCallback(debounce(async (searchTerm: string, currentResults: CombinedResults) => {
// First, query the Jellyfin API
const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));
// Convert the current results to album ids
const albumIds = currentResults.map(item => item.id);
// Parse the result in correct typescript form
const results = (payload as { results: (Album | AlbumTrack)[] }).results;
// Filter any results that are already displayed
const items = results.filter(item => (
!(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
// Then convert the results to proper result form
)).map((item) => ({
type: item.Type,
id: item.Id,
album: item.Type === 'Audio'
? item.AlbumId
: undefined,
name: item.Type === 'Audio'
? item.Name
: undefined,
}));
// Lastly, we'll merge the two and assign them to the state
setJellyfinResults([...items] as CombinedResults);
// Loading is now complete
setLoading(false);
}, 50), [dispatch, setJellyfinResults]);
/**
* Whenever the search term changes, we gather results from Fuse and assign
* them to state
*/
useEffect(() => {
if (!searchTerm) {
return;
}
const retrieveResults = async () => {
// GUARD: In some extraordinary cases, Fuse might not be presented since
// it is assigned via refs. In this case, we can't handle any searching.
if (!fuse.current) {
return;
}
// First set the immediate results from fuse
const fuseResults = fuse.current.search(searchTerm);
const albums: AlbumResult[] = fuseResults
.map(({ item }) => ({
id: item.Id,
type: 'AlbumArtist',
album: undefined,
name: undefined,
}));
// Assign the preliminary results
setFuseResults(albums);
setLoading(true);
try {
// Wrap the call in a try/catch block so that we catch any
// network issues in search and just use local search if the
// network is unavailable
fetchJellyfinResults(searchTerm, albums);
} catch {
// Reset the loading indicator if the network fails
setLoading(false);
}
};
retrieveResults();
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
// Handlers
const selectAlbum = useCallback((id: string) => {
navigation.navigate('Album', { id, album: albums[id] as Album });
}, [navigation, albums]);
const SearchInput = React.useMemo(() => (
<Animated.View style={[
{ position: 'absolute', bottom: offsets.bottom, right: 0, left: 0 },
{ transform: [{ translateY: keyboardHeight }] },
]}>
<ColoredBlurView>
<Container style={[ defaultStyles.border ]}>
<View>
<Input
value={searchTerm}
onChangeText={setSearchTerm}
style={[defaultStyles.view, { marginBottom: 12 }]}
placeholder={t('search') + '...'}
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
testID="search-input"
/>
{isLoading && <Loading style={{ marginTop: -4 }}><ActivityIndicator /></Loading>}
</View>
</Container>
{/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
<SelectableFilter
text="Artists"
icon={MicrophoneIcon}
active
/>
<SelectableFilter
text="Albums"
icon={AlbumIcon}
active={false}
/>
<SelectableFilter
text="Tracks"
icon={TrackIcon}
active={false}
/>
<SelectableFilter
text="Playlist"
icon={PlaylistIcon}
active={false}
/>
<SelectableFilter
text="Streaming"
icon={StreamIcon}
active={false}
/>
<SelectableFilter
text="Local Playback"
icon={LocalIcon}
active={false}
/>
</View>
</ScrollView> */}
</ColoredBlurView>
</Animated.View>
), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, offsets]);
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
// Therefore we delay rendering to when we are certain it's there.
if (!fuseIsReady) {
return null;
}
return (
<SafeAreaView style={{ flex: 1 }}>
<FlatList
style={{ flex: 2, }}
contentContainerStyle={{ paddingTop: offsets.top, paddingBottom: offsets.bottom + SEARCH_INPUT_HEIGHT }}
scrollIndicatorInsets={{ top: offsets.top / 2, bottom: offsets.bottom / 2 + 10 + SEARCH_INPUT_HEIGHT }}
data={[...jellyfinResults, ...fuseResults]}
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
const album = albums[trackAlbum || id];
// GUARD: If the album cannot be found in the store, we
// cannot display it.
if (!album) {
return null;
}
return (
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
<SearchResult>
<ShadowWrapper>
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
<View style={{ flex: 1 }}>
<Text numberOfLines={1}>
{trackName || album.Name}
</Text>
{(album.AlbumArtist || album.Name) && (
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
{type === 'AlbumArtist'
? `${t('album')}${album.AlbumArtist}`
: `${t('track')}${album.AlbumArtist}${album.Name}`
}
</HalfOpacity>
)}
</View>
<View style={{ marginLeft: 16 }}>
<DownloadIcon trackId={id} />
</View>
<View style={{ marginLeft: 16 }}>
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
</View>
</SearchResult>
</TouchableHandler>
);
}}
keyExtractor={(item) => item.id}
extraData={[searchTerm, albums]}
/>
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
<FullSizeContainer>
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
</FullSizeContainer>
) : null}
{SearchInput}
</SafeAreaView>
);
}

View File

@@ -4,15 +4,15 @@ import music from 'store/music';
import { t } from '@localisation'; import { t } from '@localisation';
import Button from 'components/Button'; import Button from 'components/Button';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { Text } from 'components/Typography'; import { Paragraph } from 'components/Typography';
import { useAppDispatch } from 'store'; import { useAppDispatch } from 'store';
import { SafeScrollView } from 'components/SafeNavigatorView';
const ClearCache = styled(Button)` const ClearCache = styled(Button)`
margin-top: 16px; margin-top: 16px;
`; `;
const Container = styled.ScrollView` const Container = styled(SafeScrollView)`
padding: 24px; padding: 24px;
`; `;
@@ -28,7 +28,7 @@ export default function CacheSettings() {
return ( return (
<Container> <Container>
<Text>{t('setting-cache-description')}</Text> <Paragraph>{t('setting-cache-description')}</Paragraph>
<ClearCache title={t('reset-cache')} onPress={handleClearCache} /> <ClearCache title={t('reset-cache')} onPress={handleClearCache} />
</Container> </Container>
); );

View File

@@ -6,8 +6,8 @@ import { NavigationProp } from '../..';
import { useTypedSelector } from 'store'; import { useTypedSelector } from 'store';
import { t } from '@localisation'; import { t } from '@localisation';
import Button from 'components/Button'; import Button from 'components/Button';
import { Text } from 'components/Typography'; import { Paragraph } from 'components/Typography';
import { SafeScrollView } from 'components/SafeNavigatorView';
const InputContainer = styled.View` const InputContainer = styled.View`
margin: 10px 0; margin: 10px 0;
@@ -19,7 +19,7 @@ const Input = styled.TextInput`
border-radius: 5px; border-radius: 5px;
`; `;
const Container = styled.ScrollView` const Container = styled(SafeScrollView)`
padding: 24px; padding: 24px;
`; `;
@@ -32,15 +32,15 @@ export default function LibrarySettings() {
return ( return (
<Container> <Container>
<InputContainer> <InputContainer>
<Text style={defaultStyles.text}>{t('jellyfin-server-url')}</Text> <Paragraph style={defaultStyles.text}>{t('jellyfin-server-url')}</Paragraph>
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} /> <Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} />
</InputContainer> </InputContainer>
<InputContainer> <InputContainer>
<Text style={defaultStyles.text}>{t('jellyfin-access-token')}</Text> <Paragraph style={defaultStyles.text}>{t('jellyfin-access-token')}</Paragraph>
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} /> <Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} />
</InputContainer> </InputContainer>
<InputContainer> <InputContainer>
<Text style={defaultStyles.text}>{t('jellyfin-user-id')}</Text> <Paragraph style={defaultStyles.text}>{t('jellyfin-user-id')}</Paragraph>
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} /> <Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} />
</InputContainer> </InputContainer>
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} /> <Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} />

View File

@@ -1,4 +1,4 @@
import { Text } from 'components/Typography'; import { Paragraph, Text } from 'components/Typography';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native-gesture-handler'; import { Switch } from 'react-native-gesture-handler';
@@ -9,8 +9,9 @@ import ChevronIcon from 'assets/icons/chevron-right.svg';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import useDefaultStyles, { DefaultStylesProvider } from 'components/Colors'; import useDefaultStyles, { DefaultStylesProvider } from 'components/Colors';
import { t } from '@localisation'; import { t } from '@localisation';
import { SafeScrollView } from 'components/SafeNavigatorView';
const Container = styled.ScrollView` const Container = styled.View`
padding: 24px; padding: 24px;
`; `;
@@ -25,7 +26,9 @@ const HeaderContainer = styled.View<{ isActive?: boolean }>`
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin: 16px 0 4px 0; padding: 16px 24px;
border-radius: 8px;
overflow: hidden;
${props => props.isActive && css` ${props => props.isActive && css`
background-color: ${THEME_COLOR}; background-color: ${THEME_COLOR};
@@ -37,7 +40,8 @@ const HeaderText = styled(Text)`
`; `;
const ContentContainer = styled.View` const ContentContainer = styled.View`
margin-top: 8px; margin-bottom: 8px;
padding: 8px 24px;
`; `;
const Label = styled(Text)` const Label = styled(Text)`
@@ -87,7 +91,7 @@ function renderHeader(question: Question, index: number, isActive: boolean) {
function renderContent(question: Question) { function renderContent(question: Question) {
return ( return (
<ContentContainer> <ContentContainer>
<Text>{question.content}</Text> <Paragraph>{question.content}</Paragraph>
</ContentContainer> </ContentContainer>
); );
} }
@@ -104,14 +108,17 @@ export default function Sentry() {
}); });
return ( return (
<Container> <SafeScrollView>
<Text>{t('error-reporting-description')}</Text> <Container>
<Text /> <Paragraph>{t('error-reporting-description')}</Paragraph>
<Text>{t('error-reporting-rationale')}</Text> <Paragraph />
<SwitchContainer> <Paragraph>{t('error-reporting-rationale')}</Paragraph>
<Label>{t('error-reporting')}</Label>
<Switch value={isReportingEnabled} onValueChange={toggleSwitch} /> <SwitchContainer>
</SwitchContainer> <Label>{t('error-reporting')}</Label>
<Switch value={isReportingEnabled} onValueChange={toggleSwitch} />
</SwitchContainer>
</Container>
<Accordion <Accordion
sections={questions} sections={questions}
renderHeader={renderHeader} renderHeader={renderHeader}
@@ -120,6 +127,6 @@ export default function Sentry() {
onChange={setActiveSections} onChange={setActiveSections}
underlayColor={defaultStyles.activeBackground.backgroundColor} underlayColor={defaultStyles.activeBackground.backgroundColor}
/> />
</Container> </SafeScrollView>
); );
} }

View File

@@ -1,8 +1,8 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { SafeAreaView, ScrollView } from 'react-native'; import { StyleSheet } from 'react-native';
import Library from './components/Library'; import Library from './components/Library';
import Cache from './components/Cache'; import Cache from './components/Cache';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { t } from '@localisation'; import { t } from '@localisation';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
@@ -10,6 +10,7 @@ import ListButton from 'components/ListButton';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import Sentry from './components/Sentry'; import Sentry from './components/Sentry';
import { SettingsNavigationProp } from './types'; import { SettingsNavigationProp } from './types';
import { SafeScrollView } from 'components/SafeNavigatorView';
export function SettingsList() { export function SettingsList() {
const navigation = useNavigation<SettingsNavigationProp>(); const navigation = useNavigation<SettingsNavigationProp>();
@@ -18,13 +19,11 @@ export function SettingsList() {
const handleSentryClick = useCallback(() => { navigation.navigate('Sentry'); }, [navigation]); const handleSentryClick = useCallback(() => { navigation.navigate('Sentry'); }, [navigation]);
return ( return (
<ScrollView> <SafeScrollView>
<SafeAreaView> <ListButton onPress={handleLibraryClick}>{t('jellyfin-library')}</ListButton>
<ListButton onPress={handleLibraryClick}>{t('jellyfin-library')}</ListButton> <ListButton onPress={handleCacheClick}>{t('setting-cache')}</ListButton>
<ListButton onPress={handleCacheClick}>{t('setting-cache')}</ListButton> <ListButton onPress={handleSentryClick}>{t('error-reporting')}</ListButton>
<ListButton onPress={handleSentryClick}>{t('error-reporting')}</ListButton> </SafeScrollView>
</SafeAreaView>
</ScrollView>
); );
} }
@@ -34,9 +33,11 @@ export default function Settings() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
return ( return (
<Stack.Navigator initialRouteName="SettingList" screenOptions={{ <Stack.Navigator initialRouteName="SettingList" screenOptions={{
headerTintColor: THEME_COLOR, headerTintColor: THEME_COLOR,
headerTitleStyle: defaultStyles.stackHeader headerTitleStyle: defaultStyles.stackHeader,
headerTransparent: true,
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
}}> }}>
<Stack.Screen name="SettingList" component={SettingsList} options={{ headerTitle: t('settings') }} /> <Stack.Screen name="SettingList" component={SettingsList} options={{ headerTitle: t('settings') }} />
<Stack.Screen name="Library" component={Library} options={{ headerTitle: t('jellyfin-library') }} /> <Stack.Screen name="Library" component={Library} options={{ headerTitle: t('jellyfin-library') }} />

View File

@@ -5,7 +5,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { CompositeNavigationProp } from '@react-navigation/native'; import { CompositeNavigationProp } from '@react-navigation/native';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import Search from './Search'; import SearchStack from './Search';
import Music from './Music'; import Music from './Music';
import Settings from './Settings'; import Settings from './Settings';
import Downloads from './Downloads'; import Downloads from './Downloads';
@@ -18,13 +18,15 @@ import NotesIcon from 'assets/icons/notes.svg';
import GearIcon from 'assets/icons/gear.svg'; import GearIcon from 'assets/icons/gear.svg';
import DownloadsIcon from 'assets/icons/arrow-down-to-line.svg'; import DownloadsIcon from 'assets/icons/arrow-down-to-line.svg';
import { useTypedSelector } from 'store'; import { useTypedSelector } from 'store';
import { ModalStackParams } from './types';
import { t } from '@localisation'; import { t } from '@localisation';
import ErrorReportingAlert from 'utility/ErrorReportingAlert'; import ErrorReportingAlert from 'utility/ErrorReportingAlert';
import ErrorReportingPopup from './modals/ErrorReportingPopup'; import ErrorReportingPopup from './modals/ErrorReportingPopup';
import Player from './modals/Player'; import Player from './modals/Player';
import { StyleSheet } from 'react-native';
import { ColoredBlurView } from 'components/Colors';
import { StackParams } from './types';
const Stack = createNativeStackNavigator<ModalStackParams>(); const Stack = createNativeStackNavigator<StackParams>();
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
type Screens = { type Screens = {
@@ -48,9 +50,9 @@ function Screens() {
screenOptions={({ route }) => ({ screenOptions={({ route }) => ({
tabBarIcon: function TabBarIcon({ color, size }) { tabBarIcon: function TabBarIcon({ color, size }) {
switch (route.name) { switch (route.name) {
case 'Search': case 'SearchTab':
return <SearchIcon fill={color} height={size - 4} width={size - 4} />; return <SearchIcon fill={color} height={size - 4} width={size - 4} />;
case 'Music': case 'MusicTab':
return <NotesIcon fill={color} height={size} width={size} />; return <NotesIcon fill={color} height={size} width={size} />;
case 'Settings': case 'Settings':
return <GearIcon fill={color} height={size - 1} width={size - 1} />; return <GearIcon fill={color} height={size - 1} width={size - 1} />;
@@ -63,10 +65,15 @@ function Screens() {
tabBarActiveTintColor: THEME_COLOR, tabBarActiveTintColor: THEME_COLOR,
tabBarInactiveTintColor: 'gray', tabBarInactiveTintColor: 'gray',
headerShown: false, headerShown: false,
tabBarShowLabel: false,
tabBarStyle: { position: 'absolute' },
tabBarBackground: () => (
<ColoredBlurView style={StyleSheet.absoluteFill} />
)
})} })}
> >
<Tab.Screen name="Music" component={Music} options={{ tabBarLabel: t('music'), tabBarTestID: 'music-tab' }} /> <Tab.Screen name="MusicTab" component={Music} options={{ tabBarLabel: t('music'), tabBarTestID: 'music-tab' }} />
<Tab.Screen name="Search" component={Search} options={{ tabBarLabel: t('search'), tabBarTestID: 'search-tab' }} /> <Tab.Screen name="SearchTab" component={SearchStack} options={{ tabBarLabel: t('search'), tabBarTestID: 'search-tab' }} />
<Tab.Screen name="Downloads" component={Downloads} options={{ tabBarLabel: t('downloads'), tabBarTestID: 'downloads-tab'}} /> <Tab.Screen name="Downloads" component={Downloads} options={{ tabBarLabel: t('downloads'), tabBarTestID: 'downloads-tab'}} />
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings'), tabBarTestID: 'settings-tab' }} /> <Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings'), tabBarTestID: 'settings-tab' }} />
</Tab.Navigator> </Tab.Navigator>

View File

@@ -60,7 +60,6 @@ class CredentialGenerator extends Component<Props> {
return ( return (
<WebView <WebView
source={{ uri: serverUrl as string }} source={{ uri: serverUrl as string }}
style={{ borderRadius: 20 }}
onNavigationStateChange={this.handleStateChange} onNavigationStateChange={this.handleStateChange}
onMessage={this.handleMessage} onMessage={this.handleMessage}
ref={this.ref} ref={this.ref}

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useNavigation, StackActions, useRoute, RouteProp } from '@react-navigation/native'; import { useNavigation, StackActions, useRoute, RouteProp } from '@react-navigation/native';
import { ModalStackParams } from 'screens/types'; import { StackParams } from 'screens/types';
import { useAppDispatch, useTypedSelector } from 'store'; import { useAppDispatch, useTypedSelector } from 'store';
import { Header, SubHeader } from 'components/Typography'; import { Header, SubHeader } from 'components/Typography';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
@@ -17,7 +17,7 @@ import usePlayTracks from 'utility/usePlayTracks';
import { selectIsDownloaded } from 'store/downloads/selectors'; import { selectIsDownloaded } from 'store/downloads/selectors';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
type Route = RouteProp<ModalStackParams, 'TrackPopupMenu'>; type Route = RouteProp<StackParams, 'TrackPopupMenu'>;
const Container = styled.View` const Container = styled.View`
padding: 40px; padding: 40px;

View File

@@ -1,9 +1,16 @@
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { Album } from 'store/music/types';
export interface ModalStackParams { export type StackParams = {
[key: string]: Record<string, unknown> | undefined; [key: string]: Record<string, unknown> | undefined;
Albums: undefined;
Album: { id: string, album: Album };
Playlists: undefined;
Playlist: { id: string };
RecentAlbums: undefined;
Search: undefined;
SetJellyfinServer: undefined; SetJellyfinServer: undefined;
TrackPopupMenu: { trackId: string }; TrackPopupMenu: { trackId: string };
} };
export type ModalNavigationProp = StackNavigationProp<ModalStackParams>; export type NavigationProp = StackNavigationProp<StackParams>;

View File

@@ -46,6 +46,14 @@ export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncTh
} }
); );
export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
'/albums/single',
async (ItemId, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAlbum(credentials, ItemId) as Promise<Album>;
}
);
type SearchAndFetchResults = { type SearchAndFetchResults = {
albums: Album[]; albums: Album[];
results: (Album | AlbumTrack)[]; results: (Album | AlbumTrack)[];

View File

@@ -7,7 +7,8 @@ import {
searchAndFetchAlbums, searchAndFetchAlbums,
playlistAdapter, playlistAdapter,
fetchAllPlaylists, fetchAllPlaylists,
fetchTracksByPlaylist fetchTracksByPlaylist,
fetchAlbum
} from './actions'; } from './actions';
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit'; import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
import { Album, AlbumTrack, Playlist } from './types'; import { Album, AlbumTrack, Playlist } from './types';
@@ -69,6 +70,15 @@ const music = createSlice({
}); });
builder.addCase(fetchAllAlbums.pending, (state) => { state.albums.isLoading = true; }); builder.addCase(fetchAllAlbums.pending, (state) => { state.albums.isLoading = true; });
builder.addCase(fetchAllAlbums.rejected, (state) => { state.albums.isLoading = false; }); builder.addCase(fetchAllAlbums.rejected, (state) => { state.albums.isLoading = false; });
/**
* Fetch single album
*/
builder.addCase(fetchAlbum.fulfilled, (state, { payload }) => {
albumAdapter.upsertOne(state.albums, payload);
});
builder.addCase(fetchAlbum.pending, (state) => { state.albums.isLoading = true; });
builder.addCase(fetchAlbum.rejected, (state) => { state.albums.isLoading = false; });
/** /**
* Fetch most recent albums * Fetch most recent albums

View File

@@ -43,6 +43,8 @@ export interface Album {
Tracks?: string[]; Tracks?: string[];
lastRefreshed?: number; lastRefreshed?: number;
DateCreated: string; DateCreated: string;
Overview?: string;
Similar?: string[];
} }
export interface AlbumTrack { export interface AlbumTrack {
@@ -94,4 +96,8 @@ export interface Playlist {
MediaType: string; MediaType: string;
Tracks?: string[]; Tracks?: string[];
lastRefreshed?: number; lastRefreshed?: number;
}
export interface SimilarAlbum {
Id: string;
} }

View File

@@ -5,7 +5,7 @@ import { t } from '@localisation';
import { setReceivedErrorReportingAlert } from 'store/settings/actions'; import { setReceivedErrorReportingAlert } from 'store/settings/actions';
import { setSentryStatus } from './Sentry'; import { setSentryStatus } from './Sentry';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { ModalNavigationProp } from 'screens/types'; import { NavigationProp } from 'screens/types';
/** /**
* This will send out an alert message asking the user if they want to enable * This will send out an alert message asking the user if they want to enable
@@ -13,7 +13,7 @@ import { ModalNavigationProp } from 'screens/types';
*/ */
export default function ErrorReportingAlert() { export default function ErrorReportingAlert() {
const { hasReceivedErrorReportingAlert } = useTypedSelector(state => state.settings); const { hasReceivedErrorReportingAlert } = useTypedSelector(state => state.settings);
const navigation = useNavigation<ModalNavigationProp>(); const navigation = useNavigation<NavigationProp>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {

View File

@@ -1,6 +1,6 @@
import { Track } from 'react-native-track-player'; import { Track } from 'react-native-track-player';
import { AppState, useTypedSelector } from 'store'; import { AppState, useTypedSelector } from 'store';
import { Album, AlbumTrack } from 'store/music/types'; import { Album, AlbumTrack, SimilarAlbum } from 'store/music/types';
type Credentials = AppState['settings']['jellyfin']; type Credentials = AppState['settings']['jellyfin'];
@@ -86,8 +86,14 @@ export async function retrieveAllAlbums(credentials: Credentials) {
*/ */
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> { export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
const config = generateConfig(credentials); const config = generateConfig(credentials);
const Similar = await fetch(`${credentials?.uri}/Items/${id}/Similar?userId=${credentials?.user_id}&limit=12`, config)
.then(response => response.json() as Promise<{ Items: SimilarAlbum[] }>)
.then((albums) => albums.Items.map((a) => a.Id));
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config) return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
.then(response => response.json()); .then(response => response.json() as Promise<Album>)
.then(album => ({ ...album, Similar }));
} }
const latestAlbumsOptions = { const latestAlbumsOptions = {
@@ -96,7 +102,6 @@ const latestAlbumsOptions = {
SortOrder: 'Ascending', SortOrder: 'Ascending',
}; };
/** /**
* Retrieve the most recently added albums on the Jellyfin server * Retrieve the most recently added albums on the Jellyfin server
*/ */

View File

@@ -1,9 +1,9 @@
function ticksToDuration(ticks: number) { function ticksToDuration(ticks: number) {
const seconds = Math.round(ticks / 10000000); const seconds = Math.round(ticks / 10000000);
const minutes = Math.round(seconds / 60); const minutes = Math.floor(seconds / 60);
const hours = Math.round(minutes / 60); const hours = Math.floor(minutes / 60);
return `${hours > 0 ? hours + ':' : ''}${minutes}:${(seconds % 60).toString().padStart(2, '0')}`; return `${hours > 0 ? hours + ':' : ''}${(minutes % 60).toString().padStart(hours > 0 ? 2 : 0, '0')}:${(seconds % 60).toString().padStart(2, '0')}`;
} }
export default ticksToDuration; export default ticksToDuration;

View File

@@ -1,6 +1,6 @@
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useRef, useEffect } from 'react'; import { useRef, useEffect } from 'react';
import { Animated, Keyboard, KeyboardEvent } from 'react-native'; import { Animated, Easing, Keyboard, KeyboardEvent } from 'react-native';
/** /**
* This returns an animated height that the keyboard is poking up from the * This returns an animated height that the keyboard is poking up from the
@@ -15,9 +15,10 @@ export const useKeyboardHeight = () => {
useEffect(() => { useEffect(() => {
const keyboardWillShow = (e: KeyboardEvent) => { const keyboardWillShow = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, { Animated.timing(keyboardHeight, {
duration: e.duration, duration: e.duration - 20,
toValue: tabBarHeight - e.endCoordinates.height, toValue: tabBarHeight - e.endCoordinates.height,
useNativeDriver: true, useNativeDriver: true,
easing: Easing.ease,
}).start(); }).start();
}; };