Compare commits

...

7 Commits

Author SHA1 Message Date
Lei Nelissen
1402ac06f6 Bump major version 2022-01-16 12:37:09 +01:00
Lei Nelissen
f9334c51a3 Release new versions 2022-01-15 17:51:15 +01:00
Lei Nelissen
5d26a5395b Install JSON plugin for fastlane 2022-01-15 17:35:28 +01:00
Lei Nelissen
714535feeb Bump version 2022-01-15 17:26:07 +01:00
Lei Nelissen
7ea4857997 Queue downloads seperately so we don't overwhelm the app 2022-01-15 17:25:24 +01:00
Lei Nelissen
81ccb6b1f9 Use package.json for app version 2022-01-15 17:24:56 +01:00
Lei Nelissen
98ae0216f7 Replace icons with filled version 2022-01-15 17:24:47 +01:00
22 changed files with 233 additions and 69 deletions

View File

@@ -136,8 +136,8 @@ android {
applicationId "com.jellyfinaudioplayer"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 5
versionName "1.2.3"
multiDexEnabled true
}
splits {

View File

@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@@ -1,18 +1,8 @@
default_platform(:ios)
package = load_json(json_path: "package.json")
platform :ios do
lane :alpha do
get_certificates(
development: true,
output_path: 'certificates/'
)
build_app(
scheme: "Jellyfin Player",
export_method: "development",
output_directory: "build",
workspace: "ios/JellyfinAudioPlayer.xcworkspace"
)
end
lane :beta do
get_certificates(
output_path: 'certificates/'
@@ -26,8 +16,12 @@ platform :ios do
use_automatic_signing: true,
path: "ios/JellyfinAudioPlayer.xcodeproj"
)
increment_version_number(
version_number: package["version"],
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
);
increment_build_number(
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj"
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
)
build_app(
scheme: "Jellyfin Player",
@@ -37,7 +31,7 @@ platform :ios do
)
upload_to_testflight
build_number = get_build_number(
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj"
xcodeproj: "ios/JellyfinAudioPlayer.xcodeproj",
)
Dir.chdir("..") do
sh(
@@ -78,8 +72,15 @@ end
platform :android do
desc "Generate beta build"
lane :beta do
android_set_version_name(
version_name: package['version'],
gradle_file: "android/app/build.gradle"
)
android_set_version_code(
gradle_file: "android/app/build.gradle"
)
gradle(
task: "clean assembleRelease",
task: "assembleRelease",
project_dir: "android"
)
end

View File

@@ -3,3 +3,5 @@
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-sentry'
gem 'fastlane-plugin-load_json'
gem 'fastlane-plugin-versioning_android'

View File

@@ -16,11 +16,6 @@ or alternatively using `brew install fastlane`
# Available Actions
## iOS
### ios alpha
```
fastlane ios alpha
```
### ios beta
```
fastlane ios beta

View File

@@ -554,7 +554,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 31;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 238P3C58WC;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -590,7 +590,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 31;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 238P3C58WC;
INFOPLIST_FILE = JellyfinAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.2.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>31</string>
<string>34</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.2.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>31</string>
<string>34</string>
</dict>
</plist>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "JellyfinAudioPlayer",
"version": "0.2.0",
"version": "1.2.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "JellyfinAudioPlayer",
"version": "0.2.0",
"version": "1.2.3",
"dependencies": {
"@react-native-community/async-storage": "^1.12.1",
"@react-native-community/masked-view": "^0.1.11",

View File

@@ -1,6 +1,6 @@
{
"name": "JellyfinAudioPlayer",
"version": "0.2.2",
"version": "1.2.3",
"main": "src/index.js",
"private": true,
"scripts": {

View File

@@ -1,3 +1,3 @@
<svg viewBox="0 0 24 16" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3281 15.6592C21.0703 15.6592 23.2324 13.6465 23.2324 11.1328C23.2324 9.26074 22.1689 7.59961 20.4199 6.87012C20.4287 2.89746 17.5635 0.0322266 13.8809 0.0322266C11.543 0.0322266 9.78516 1.23633 8.68652 2.83594C6.4541 2.23828 4.10742 3.89941 4.01953 6.37793C2.00684 6.73828 0.767578 8.54004 0.767578 10.7461C0.767578 13.418 3.10547 15.6504 6.18164 15.6504L18.3281 15.6592ZM18.3281 13.9014H6.19043C4.09863 13.9014 2.54297 12.4424 2.54297 10.7461C2.54297 8.98828 3.62402 7.6875 5.41699 7.6875C5.54883 7.6875 5.60156 7.61719 5.59277 7.49414C5.54004 4.88379 7.41211 3.9873 9.30176 4.58496C9.41602 4.62012 9.48633 4.59375 9.53906 4.49707C10.4092 2.96777 11.6924 1.78125 13.8721 1.78125C16.6318 1.78125 18.6006 3.96973 18.7324 6.52734C18.7588 7.00195 18.7236 7.51172 18.6885 7.93359C18.6709 8.05664 18.7236 8.12695 18.8379 8.14453C20.4287 8.45215 21.457 9.57715 21.457 11.1328C21.457 12.6709 20.0947 13.9014 18.3281 13.9014Z" />
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3281 15.6504C21.0703 15.6504 23.2324 13.6377 23.2324 11.124C23.2324 9.26074 22.1689 7.59961 20.4199 6.87012C20.4287 2.88867 17.5635 0.0234375 13.8809 0.0234375C11.543 0.0234375 9.78516 1.23633 8.68652 2.82715C6.4541 2.22949 4.10742 3.89062 4.01953 6.36914C2.00684 6.72949 0.767578 8.54004 0.767578 10.7373C0.767578 13.418 3.10547 15.6504 6.18164 15.6504H18.3281Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -1,3 +1,3 @@
<svg viewBox="0 0 22 17" xmlns="http://www.w3.org/2000/svg">
<path d="M0.400391 12.2139C0.400391 14.5781 2.17578 16.3535 4.71582 16.3535H17.2842C19.8242 16.3535 21.5996 14.5781 21.5996 12.2139C21.5996 11.502 21.3975 10.8604 21.1514 10.2803L18.1279 3.19629C17.5127 1.74609 16.291 0.981445 14.6562 0.981445H7.35254C5.70898 0.981445 4.4873 1.74609 3.88086 3.19629L0.875 10.2451C0.620117 10.834 0.400391 11.4844 0.400391 12.2139ZM3.57324 8.26758L5.48926 3.5918C5.78809 2.83594 6.46484 2.44043 7.37012 2.44043H14.6299C15.5439 2.44043 16.2207 2.83594 16.5195 3.5918L18.4355 8.26758C18.084 8.15332 17.6973 8.08301 17.2842 8.08301H4.71582C4.30273 8.08301 3.9248 8.15332 3.57324 8.26758ZM2.08789 12.2139C2.08789 10.8164 3.13379 9.77051 4.71582 9.77051H17.2842C18.8662 9.77051 19.9121 10.8164 19.9121 12.2139C19.9121 13.7607 18.8662 14.6572 17.2842 14.6572H4.71582C3.13379 14.6572 2.08789 13.6201 2.08789 12.2139ZM8.45117 13.084C8.45117 13.3828 8.68848 13.6113 8.9873 13.6113C9.27734 13.6113 9.50586 13.3828 9.50586 13.084V11.3525C9.50586 11.0625 9.27734 10.8252 8.9873 10.8252C8.68848 10.8252 8.45117 11.0625 8.45117 11.3525V13.084ZM10.4727 13.084C10.4727 13.3828 10.7012 13.6113 11 13.6113C11.29 13.6113 11.5273 13.3828 11.5273 13.084V11.3525C11.5273 11.0625 11.29 10.8252 11 10.8252C10.7012 10.8252 10.4727 11.0625 10.4727 11.3525V13.084ZM12.4854 13.084C12.4854 13.3828 12.7227 13.6113 13.0215 13.6113C13.3115 13.6113 13.5488 13.3828 13.5488 13.084V11.3525C13.5488 11.0625 13.3115 10.8252 13.0215 10.8252C12.7227 10.8252 12.4854 11.0625 12.4854 11.3525V13.084ZM14.5068 13.084C14.5068 13.3828 14.7441 13.6113 15.043 13.6113C15.333 13.6113 15.5703 13.3828 15.5703 13.084V11.3525C15.5703 11.0625 15.333 10.8252 15.043 10.8252C14.7441 10.8252 14.5068 11.0625 14.5068 11.3525V13.084ZM16.5283 13.084C16.5283 13.3828 16.7656 13.6113 17.0645 13.6113C17.3545 13.6113 17.583 13.3828 17.583 13.084V11.3525C17.583 11.0625 17.3545 10.8252 17.0645 10.8252C16.7656 10.8252 16.5283 11.0625 16.5283 11.3525V13.084Z" />
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.33789 6.74023H17.6621C18.5498 6.74023 19.3496 6.94238 20.0439 7.30273L17.873 2.21387C17.293 0.851562 16.1152 0.0957031 14.542 0.0957031H7.45801C5.87598 0.0957031 4.70703 0.851562 4.12695 2.21387L1.94727 7.29395C2.6416 6.94238 3.4502 6.74023 4.33789 6.74023ZM4.33789 15.2305H17.6621C19.8066 15.2305 21.2832 13.7627 21.2832 11.6357C21.2832 9.5 19.8066 8.03223 17.6621 8.03223H4.33789C2.18457 8.03223 0.708008 9.5 0.708008 11.6357C0.708008 13.7627 2.18457 15.2305 4.33789 15.2305ZM16.4316 11.6357C16.4316 11.0029 16.9678 10.4668 17.6094 10.4668C18.2422 10.4668 18.7783 11.0029 18.7783 11.6357C18.7783 12.2773 18.2422 12.7959 17.6094 12.7959C16.9678 12.8047 16.4316 12.2861 16.4316 11.6357Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 806 B

View File

@@ -11,6 +11,7 @@ import {
} from '@react-navigation/native';
import { useColorScheme } from 'react-native';
import { ColorSchemeContext, themes } from './Colors';
import DownloadManager from './DownloadManager';
// import ErrorReportingAlert from 'utility/ErrorReportingAlert';
export default function App(): JSX.Element {
@@ -41,6 +42,7 @@ export default function App(): JSX.Element {
<ColorSchemeContext.Provider value={theme}>
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Routes />
<DownloadManager />
</NavigationContainer>
</ColorSchemeContext.Provider>
</PersistGate>

View File

@@ -1,12 +1,14 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { useTypedSelector } from 'store';
import CloudIcon from 'assets/cloud.svg';
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
import CloudExclamationMarkIcon from 'assets/cloud-exclamation-mark.svg';
import InternalDriveIcon from 'assets/internal-drive.svg';
import useDefaultStyles from './Colors';
import { EntityId } from '@reduxjs/toolkit';
import Svg, { Circle, CircleProps } from 'react-native-svg';
import { Animated, Easing } from 'react-native';
import styled from 'styled-components/native';
interface DownloadIconProps {
trackId: EntityId;
@@ -14,6 +16,17 @@ interface DownloadIconProps {
fill?: string;
}
const DownloadContainer = styled.View`
position: relative;
`;
const IconOverlay = styled.View`
position: absolute;
top: 0;
left: 0;
transform: scale(0.5);
`;
function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
// determine styles
const defaultStyles = useDefaultStyles();
@@ -21,6 +34,7 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
// Get download icon from state
const entity = useTypedSelector((state) => state.downloads.entities[trackId]);
const isQueued = useTypedSelector((state) => state.downloads.queued.includes(trackId));
// Memoize calculations for radius and circumference of the circle
const radius = useMemo(() => size / 2, [size]);
@@ -52,43 +66,46 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
return () => offsetAnimation.removeListener(subscription);
}, [offsetAnimation]);
if (!entity) {
if (!entity && !isQueued) {
return (
<CloudIcon width={size} height={size} fill={iconFill} />
);
}
const { isComplete, isFailed } = entity;
if (isComplete) {
if (entity?.isComplete) {
return (
<InternalDriveIcon width={size} height={size} fill={iconFill} />
);
}
if (isFailed) {
if (entity?.isFailed) {
return (
<CloudExclamationMarkIcon width={size} height={size} fill={iconFill} />
);
}
if (!isComplete && !isFailed) {
if (isQueued || (!entity?.isFailed && !entity?.isComplete)) {
return (
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
<Circle
cx={radius}
cy={radius}
r={radius - 1}
stroke={iconFill}
// @ts-expect-error react-native-svg has outdated react-native typings
ref={circleRef}
strokeWidth={1.5}
strokeDasharray={[ circumference, circumference ]}
strokeDashoffset={circumference}
strokeLinecap='round'
fill='transparent'
/>
</Svg>
<DownloadContainer>
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
<Circle
cx={radius}
cy={radius}
r={radius - 1}
stroke={iconFill}
// @ts-expect-error react-native-svg has outdated react-native typings
ref={circleRef}
strokeWidth={1.5}
strokeDasharray={[ circumference, circumference ]}
strokeDashoffset={circumference}
strokeLinecap='round'
fill='transparent'
/>
</Svg>
<IconOverlay>
<CloudDownArrow width={size} height={size} fill={iconFill} />
</IconOverlay>
</DownloadContainer>
);
}

View File

@@ -0,0 +1,110 @@
import { EntityId } from '@reduxjs/toolkit';
import { xor } from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { DocumentDirectoryPath, readDir } from 'react-native-fs';
import { useDispatch } from 'react-redux';
import { useTypedSelector } from 'store';
import { completeDownload, downloadTrack } from 'store/downloads/actions';
/**
* The maximum number of concurrent downloads we allow to take place at once.
* This is hardcoded at 5 for now, but might be extracted to a setting later.
*/
const MAX_CONCURRENT_DOWNLOADS = 5;
/**
* This is a component that tracks queued downloads, and starts them one-by-one,
* so that we don't overload react-native-fs, as well as the render performance.
*/
function DownloadManager () {
// Retrieve store helpers
const { queued, ids } = useTypedSelector((state) => state.downloads);
const rehydrated = useTypedSelector((state) => state._persist.rehydrated);
const dispatch = useDispatch();
// Keep state for the currently active downloads (i.e. the downloads that
// have actually been pushed out to react-native-fs).
const [hasRehydratedOrphans, setHasRehydratedOrphans] = useState(false);
const activeDownloads = useRef(new Set<EntityId>());
useEffect(() => {
// GUARD: Check if the queue is empty
if (!queued.length) {
// If so, clear any current downloads
activeDownloads.current.clear();
return;
}
// Apparently, the queue has changed, and we need to manage
// First, we pick the first n downloads
const queue = queued.slice(0, MAX_CONCURRENT_DOWNLOADS);
// We then filter for new downloads
queue.filter((id) => !activeDownloads.current.has(id))
.forEach((id) => {
// We dispatch the actual call to start downloading
dispatch(downloadTrack(id));
// And add it to the active downloads
activeDownloads.current.add(id);
});
// Lastly, if something isn't part of the queue, but is of active
// downloads, we can assume the download completed.
xor(Array.from(activeDownloads.current), queue)
.forEach((id) => activeDownloads.current.delete(id));
}, [queued, dispatch, activeDownloads]);
useEffect(() => {
// GUARD: We only run this functino once
if (hasRehydratedOrphans) {
return;
}
// GUARD: If the state has not been rehydrated, we cannot check against
// the store ids.
if (!rehydrated) {
return;
}
console.log(ids);
/**
* Whenever the store is cleared, existing downloads get "lost" because
* the only reference we have is the store. This function checks for
* those lost downloads and adds them to the store
*/
async function hydrateOrphanedDownloads() {
// Retrieve all files for this app
const files = await readDir(DocumentDirectoryPath);
// Loop through the mp3 files
files.filter((file) => file.isFile() && file.name.endsWith('.mp3'))
.forEach((file) => {
const id = file.name.replace('.mp3', '');
console.log(id, ids.includes(id));
// GUARD: If the id is already in the store, there's nothing
// left for us to do.
if (ids.includes(id)) {
return;
}
// Add the download to the store
dispatch(completeDownload({
id,
location: file.path,
size: Number.parseInt(file.size),
}));
});
}
hydrateOrphanedDownloads();
setHasRehydratedOrphans(true);
}, [rehydrated, ids, hasRehydratedOrphans, dispatch]);
return null;
}
export default DownloadManager;

View File

@@ -51,7 +51,7 @@
"download-playlist": "Download Playlist",
"no-downloads": "Je hebt nog geen nummers gedownload",
"delete-track": "Verwijder Track",
"delete-all-tracks": "Verwijder alle nummbers",
"delete-all-tracks": "Verwijder alle nummers",
"delete-album": "Verwijder Album",
"delete-playlist": "Verwijder Playlist",
"total-download-size": "Totale grootte downloads",

View File

@@ -10,7 +10,7 @@ import ArrowClockwise from 'assets/arrow-clockwise.svg';
import { THEME_COLOR } from 'CONSTANTS';
import { useDispatch } from 'react-redux';
import { EntityId } from '@reduxjs/toolkit';
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
import Button from 'components/Button';
import { t } from 'i18n-js';
import DownloadIcon from 'components/DownloadIcon';
@@ -51,7 +51,7 @@ function Downloads() {
// Retry a single failed track
const retryTrack = useCallback((id: EntityId) => {
dispatch(downloadTrack(id));
dispatch(queueTrackForDownload(id));
}, [dispatch]);
// Retry all failed tracks

View File

@@ -20,7 +20,7 @@ import DownloadIcon from 'components/DownloadIcon';
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
import Trash from 'assets/trash.svg';
import { useDispatch } from 'react-redux';
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
import { selectDownloadedTracks } from 'store/downloads/selectors';
const Screen = Dimensions.get('screen');
@@ -111,7 +111,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index] });
}, [navigation, trackIds]);
const downloadAllTracks = useCallback(() => {
trackIds.forEach((trackId) => dispatch(downloadTrack(trackId)));
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
}, [dispatch, trackIds]);
const deleteAllTracks = useCallback(() => {
downloadedTracks.forEach((trackId) => dispatch(removeDownloadedTrack(trackId)));

View File

@@ -13,7 +13,7 @@ import TrashIcon from 'assets/trash.svg';
import Text from 'components/Text';
import { WrappableButton, WrappableButtonRow } from 'components/WrappableButtonRow';
import { useDispatch } from 'react-redux';
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
import usePlayTracks from 'utility/usePlayTracks';
import { selectIsDownloaded } from 'store/downloads/selectors';
@@ -57,7 +57,7 @@ function TrackPopupMenu() {
// Callback for downloading the track
const handleDownload = useCallback(() => {
dispatch(downloadTrack(trackId));
dispatch(queueTrackForDownload(trackId));
closeModal();
}, [trackId, dispatch, closeModal]);

View File

@@ -8,6 +8,7 @@ export const downloadAdapter = createEntityAdapter<DownloadEntity>({
selectId: (entity) => entity.id,
});
export const queueTrackForDownload = createAction<EntityId>('download/queue');
export const initializeDownload = createAction<{ id: EntityId, size?: number, jobId?: number }>('download/initialize');
export const progressDownload = createAction<{ id: EntityId, progress: number, jobId?: number }>('download/progress');
export const completeDownload = createAction<{ id: EntityId, location: string, size?: number }>('download/complete');
@@ -46,7 +47,7 @@ export const downloadTrack = createAsyncThunk(
);
export const removeDownloadedTrack = createAsyncThunk(
'/downloads/track/remove',
'/downloads/remove/track',
async(id: EntityId) => {
return unlink(`${DocumentDirectoryPath}/${id}.mp3`);
}

View File

@@ -1,15 +1,25 @@
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
import { completeDownload, downloadAdapter, failDownload, initializeDownload, progressDownload, removeDownloadedTrack } from './actions';
import {
completeDownload,
downloadAdapter,
failDownload,
initializeDownload,
progressDownload,
queueTrackForDownload,
removeDownloadedTrack
} from './actions';
import { DownloadEntity } from './types';
interface State {
entities: Dictionary<DownloadEntity>;
ids: EntityId[];
queued: EntityId[];
}
export const initialState: State = {
entities: {},
ids: [],
queued: [],
};
const downloads = createSlice({
@@ -32,6 +42,7 @@ const downloads = createSlice({
});
});
builder.addCase(completeDownload, (state, action) => {
// Update the item to be completed
downloadAdapter.updateOne(state, {
id: action.payload.id,
changes: {
@@ -40,6 +51,11 @@ const downloads = createSlice({
isComplete: true,
}
});
// Remove the item from the queue
const newSet = new Set(state.queued);
newSet.delete(action.payload.id);
state.queued = Array.from(newSet);
});
builder.addCase(failDownload, (state, action) => {
downloadAdapter.updateOne(state, {
@@ -52,7 +68,17 @@ const downloads = createSlice({
});
});
builder.addCase(removeDownloadedTrack.fulfilled, (state, action) => {
// Remove the download if it exists
downloadAdapter.removeOne(state, action.meta.arg);
// Remove the item from the queue if it is in there
const newSet = new Set(state.queued);
newSet.delete(action.meta.arg);
state.queued = Array.from(newSet);
});
builder.addCase(queueTrackForDownload, (state, action) => {
const newSet = new Set(state.queued).add(action.payload);
state.queued = Array.from(newSet);
});
},
});

View File

@@ -9,10 +9,10 @@ import music, { initialState as musicInitialState } from './music';
import downloads, { initialState as downloadsInitialState } from './downloads';
import { PersistState } from 'redux-persist/es/types';
const persistConfig: PersistConfig<AppState> = {
const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
key: 'root',
storage: AsyncStorage,
version: 1,
version: 2,
stateReconciler: autoMergeLevel2,
migrate: createMigrate({
// @ts-expect-error migrations are poorly typed
@@ -23,6 +23,16 @@ const persistConfig: PersistConfig<AppState> = {
downloads: downloadsInitialState,
music: musicInitialState
};
},
// @ts-expect-error migrations are poorly typed
2: (state: AppState) => {
return {
...state,
downloads: {
...state.downloads,
queued: []
}
};
}
})
};
@@ -48,7 +58,7 @@ const store = configureStore({
),
});
export type AppState = ReturnType<typeof reducers>;
export type AppState = ReturnType<typeof reducers> & { _persist: PersistState };
export type AppDispatch = typeof store.dispatch;
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;