diff --git a/ios/JellyfinAudioPlayer/AppDelegate.m b/ios/JellyfinAudioPlayer/AppDelegate.m index 7d6914f..3511274 100644 --- a/ios/JellyfinAudioPlayer/AppDelegate.m +++ b/ios/JellyfinAudioPlayer/AppDelegate.m @@ -11,6 +11,7 @@ #import #import #import +#import static void InitializeFlipper(UIApplication *application) { FlipperClient *client = [FlipperClient sharedClient]; @@ -59,4 +60,9 @@ static void InitializeFlipper(UIApplication *application) { #endif } +- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler +{ + [RNFSManager setCompletionHandlerForIdentifier:identifier completionHandler:completionHandler]; +} + @end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b7049e9..6d31be4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -283,6 +283,8 @@ PODS: - glog - react-native-airplay-button (1.1.0): - React-Core + - react-native-flipper (0.127.0): + - React-Core - react-native-safe-area-context (3.3.2): - React-Core - react-native-slider (4.1.12): @@ -367,6 +369,8 @@ PODS: - React-Core - SDWebImage (~> 5.11.1) - SDWebImageWebPCoder (~> 0.8.4) + - RNFS (2.18.0): + - React - RNGestureHandler (2.1.0): - React-Core - RNLocalize (2.1.7): @@ -463,6 +467,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-airplay-button (from `../node_modules/react-native-airplay-button`) + - react-native-flipper (from `../node_modules/react-native-flipper`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-track-player (from `../node_modules/react-native-track-player`) @@ -483,6 +488,7 @@ DEPENDENCIES: - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" - "RNCPicker (from `../node_modules/@react-native-community/picker`)" - RNFastImage (from `../node_modules/react-native-fast-image`) + - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNLocalize (from `../node_modules/react-native-localize`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -550,6 +556,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-airplay-button: :path: "../node_modules/react-native-airplay-button" + react-native-flipper: + :path: "../node_modules/react-native-flipper" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-slider: @@ -590,6 +598,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/picker" RNFastImage: :path: "../node_modules/react-native-fast-image" + RNFS: + :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNLocalize: @@ -638,6 +648,7 @@ SPEC CHECKSUMS: React-jsinspector: d0374f7509d407d2264168b6d0fad0b54e300b85 React-logger: 933f80c97c633ee8965d609876848148e3fef438 react-native-airplay-button: 90c7ba52402c8e92342003b8a1ff78dfb4357a9e + react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057 react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81 react-native-track-player: 23dd515aacf1d36a0e522ef7fdbc55f13f26d4fb @@ -658,6 +669,7 @@ SPEC CHECKSUMS: RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNCPicker: 914b557e20b3b8317b084aca9ff4b4edb95f61e4 RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7 + RNFS: 3ab21fa6c56d65566d1fb26c2228e2b6132e5e32 RNGestureHandler: e5c7cab5f214503dcefd6b2b0cefb050e1f51c4a RNLocalize: f567ea0e35116a641cdffe6683b0d212d568f32a RNReanimated: da3860204e5660c0dd66739936732197d359d753 diff --git a/package-lock.json b/package-lock.json index 4bd3441..c880c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,8 @@ "react-native-collapsible": "^1.6.0", "react-native-dotenv": "^3.3.1", "react-native-fast-image": "^8.5.11", + "react-native-flipper": "^0.127.0", + "react-native-fs": "^2.18.0", "react-native-gesture-handler": "^2.1.0", "react-native-localize": "^2.1.7", "react-native-reanimated": "^2.3.1", @@ -40,6 +42,7 @@ "react-native-webview": "^11.15.0", "react-redux": "^7.2.6", "redux": "^4.1.2", + "redux-flipper": "^2.0.1", "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", "styled-components": "^5.3.3" @@ -5306,6 +5309,11 @@ "node": ">=0.10.0" } }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, "node_modules/base/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", @@ -6048,6 +6056,14 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -14002,6 +14018,28 @@ "react-native": ">=0.60.0" } }, + "node_modules/react-native-flipper": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/react-native-flipper/-/react-native-flipper-0.127.0.tgz", + "integrity": "sha512-qloUyUOs9MoMVncIDDWeOxAPbomWJ3e4y0SgyCgq8joJEOXC7RvPWeEfUXp0EPyNhHGQV9a4RwzF6BWKFCR3Kg==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-native": ">0.62.0" + } + }, + "node_modules/react-native-fs": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.18.0.tgz", + "integrity": "sha512-9iQhkUNnN2JNED0in06JwZy88YEVyIGKWz4KLlQYxa5Y2U0U2AZh9FUHtA04oWj+xt2LlHh0LFPCzhmNsAsUDg==", + "dependencies": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + }, + "peerDependencies": { + "react-native": "*", + "react-native-windows": "*" + } + }, "node_modules/react-native-gesture-handler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.1.0.tgz", @@ -14352,6 +14390,20 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-flipper": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/redux-flipper/-/redux-flipper-2.0.1.tgz", + "integrity": "sha512-JqgnL+fUp3h2fPQRszItLkarTbIf5gjU4Sn0IY/ZxUo6oUUmMh4Lx2J7BBy78cE8KEW9prRRpvJ6NH2uc/QktA==", + "dependencies": { + "cycle": "^1.0.3", + "dayjs": "^1.8.29" + }, + "peerDependencies": { + "react-native": ">=0.63.0", + "react-native-flipper": ">=0.100.0", + "redux": "^4" + } + }, "node_modules/redux-logger": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", @@ -16156,6 +16208,11 @@ "node": ">=8" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -20495,6 +20552,11 @@ } } }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -21053,6 +21115,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -27265,6 +27332,21 @@ "integrity": "sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA==", "requires": {} }, + "react-native-flipper": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/react-native-flipper/-/react-native-flipper-0.127.0.tgz", + "integrity": "sha512-qloUyUOs9MoMVncIDDWeOxAPbomWJ3e4y0SgyCgq8joJEOXC7RvPWeEfUXp0EPyNhHGQV9a4RwzF6BWKFCR3Kg==", + "requires": {} + }, + "react-native-fs": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.18.0.tgz", + "integrity": "sha512-9iQhkUNnN2JNED0in06JwZy88YEVyIGKWz4KLlQYxa5Y2U0U2AZh9FUHtA04oWj+xt2LlHh0LFPCzhmNsAsUDg==", + "requires": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + } + }, "react-native-gesture-handler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.1.0.tgz", @@ -27540,6 +27622,15 @@ "@babel/runtime": "^7.9.2" } }, + "redux-flipper": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/redux-flipper/-/redux-flipper-2.0.1.tgz", + "integrity": "sha512-JqgnL+fUp3h2fPQRszItLkarTbIf5gjU4Sn0IY/ZxUo6oUUmMh4Lx2J7BBy78cE8KEW9prRRpvJ6NH2uc/QktA==", + "requires": { + "cycle": "^1.0.3", + "dayjs": "^1.8.29" + } + }, "redux-logger": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", @@ -28945,6 +29036,11 @@ "mem": "^4.3.0" } }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 4bbf448..9f1566f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "react-native-collapsible": "^1.6.0", "react-native-dotenv": "^3.3.1", "react-native-fast-image": "^8.5.11", + "react-native-flipper": "^0.127.0", + "react-native-fs": "^2.18.0", "react-native-gesture-handler": "^2.1.0", "react-native-localize": "^2.1.7", "react-native-reanimated": "^2.3.1", @@ -44,6 +46,7 @@ "react-native-webview": "^11.15.0", "react-redux": "^7.2.6", "redux": "^4.1.2", + "redux-flipper": "^2.0.1", "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", "styled-components": "^5.3.3" diff --git a/src/assets/arrow-down-to-line.svg b/src/assets/arrow-down-to-line.svg new file mode 100644 index 0000000..cdc4378 --- /dev/null +++ b/src/assets/arrow-down-to-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/cloud-down-arrow.svg b/src/assets/cloud-down-arrow.svg new file mode 100644 index 0000000..08e00fe --- /dev/null +++ b/src/assets/cloud-down-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/cloud-exclamation-mark.svg b/src/assets/cloud-exclamation-mark.svg new file mode 100644 index 0000000..4db4d07 --- /dev/null +++ b/src/assets/cloud-exclamation-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/cloud.svg b/src/assets/cloud.svg new file mode 100644 index 0000000..2e03fae --- /dev/null +++ b/src/assets/cloud.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/internal-drive.svg b/src/assets/internal-drive.svg new file mode 100644 index 0000000..2fa0ff3 --- /dev/null +++ b/src/assets/internal-drive.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/trash.svg b/src/assets/trash.svg new file mode 100644 index 0000000..eb72f89 --- /dev/null +++ b/src/assets/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 39dd6c2..bf7b762 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -51,8 +51,8 @@ const Button = React.forwardRef(function Button(props, ref) { > {Icon && state.downloads.entities[trackId]); + const iconFill = fill || defaultStyles.textHalfOpacity.color; + + + if (!entity) { + return ( + + ); + } + + const { isComplete, isFailed, progress } = entity; + + if (isComplete) { + return ( + + ); + } + + if (isFailed) { + return ( + + ); + } + + if (!isComplete && !isFailed) { + const radius = size / 2; + const circumference = radius * 2 * Math.PI; + + return ( + + + + ); + } + + return null; +} + +export default DownloadIcon; diff --git a/src/localisation/lang/en/locale.json b/src/localisation/lang/en/locale.json index 2d98441..b48406c 100644 --- a/src/localisation/lang/en/locale.json +++ b/src/localisation/lang/en/locale.json @@ -44,5 +44,12 @@ "playlist": "Playlist", "play-playlist": "Play Playlist", "shuffle-album": "Shuffle Album", - "shuffle-playlist": "Shuffle Playlist" + "shuffle-playlist": "Shuffle Playlist", + "downloads": "Downloads", + "download-track": "Download Track", + "download-album": "Download Album", + "download-playlist": "Download Playlist", + "no-downloads": "You have not yet downloaded any tracks", + "delete-all-tracks": "Delete All Tracks", + "total-download-size": "Total Download Size" } \ No newline at end of file diff --git a/src/localisation/types.ts b/src/localisation/types.ts index 076f6b9..b62563c 100644 --- a/src/localisation/types.ts +++ b/src/localisation/types.ts @@ -43,4 +43,11 @@ export type LocaleKeys = 'play-next' | 'playlist' | 'play-playlist' | 'shuffle-album' -| 'shuffle-playlist' \ No newline at end of file +| 'shuffle-playlist' +| 'downloads' +| 'download-track' +| 'download-album' +| 'download-playlist' +| 'delete-all-tracks' +| 'total-download-size' +| 'no-downloads' \ No newline at end of file diff --git a/src/screens/Downloads/index.tsx b/src/screens/Downloads/index.tsx new file mode 100644 index 0000000..837601f --- /dev/null +++ b/src/screens/Downloads/index.tsx @@ -0,0 +1,85 @@ +import useDefaultStyles from 'components/Colors'; +import React, { useCallback, useMemo } from 'react'; +import { Pressable, Text, View } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTypedSelector } from 'store'; +import formatBytes from 'utility/formatBytes'; +import TrashIcon from 'assets/trash.svg'; +import { THEME_COLOR } from 'CONSTANTS'; +import { useDispatch } from 'react-redux'; +import { EntityId } from '@reduxjs/toolkit'; +import { removeDownloadedTrack } from 'store/downloads/actions'; +import Button from 'components/Button'; +import { t } from 'i18n-js'; + +function Downloads() { + const defaultStyles = useDefaultStyles(); + const dispatch = useDispatch(); + + const { entities, ids } = useTypedSelector((state) => state.downloads); + const tracks = useTypedSelector((state) => state.music.tracks.entities); + + // Calculate the total download size + const totalDownloadSize = useMemo(() => ( + ids?.reduce((sum, id) => sum + (entities[id]?.size || 0), 0) + ), [ids, entities]); + + const handleDelete = useCallback((id: EntityId) => { + dispatch(removeDownloadedTrack(id)); + }, [dispatch]); + const handleDeleteAllTracks = useCallback(() => { + ids.forEach((id) => dispatch(removeDownloadedTrack(id))); + }, [dispatch, ids]); + + if (!ids.length) { + return ( + + + {t('no-downloads')} + + + ); + } + + return ( + + + {t('total-download-size')}: {formatBytes(totalDownloadSize)} + + } + ListFooterComponent={ +