@reduxjs/toolkit#EntityId TypeScript Examples

The following examples show how to use @reduxjs/toolkit#EntityId. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: actions.ts    From jellyfin-audio-player with MIT License 6 votes vote down vote up
downloadTrack = createAsyncThunk(
    '/downloads/track',
    async (id: EntityId, { dispatch, getState }) => {
        // Get the credentials from the store
        const { settings: { jellyfin: credentials } } = (getState() as AppState);

        // Generate the URL we can use to download the file
        const url = generateTrackUrl(id as string, credentials);
        const location = `${DocumentDirectoryPath}/${id}.mp3`;

        // Actually kick off the download
        const { promise } = await downloadFile({
            fromUrl: url,
            progressInterval: 250,
            background: true,
            begin: ({ jobId, contentLength }) => {
                // Dispatch the initialization
                dispatch(initializeDownload({ id, jobId, size: contentLength }));
            },
            progress: (result) => {
                // Dispatch a progress update
                dispatch(progressDownload({ id, progress: result.bytesWritten / result.contentLength }));
            },
            toFile: location,
        });

        // Await job completion
        const result = await promise;
        dispatch(completeDownload({ id, location, size: result.bytesWritten }));
    },
)
Example #2
Source File: selectors.ts    From jellyfin-audio-player with MIT License 6 votes vote down vote up
selectDownloadedTracks = (trackIds: EntityId[]) => (
    createSelector(
        selectAllDownloads,
        ({ entities, ids }) => {
            return intersection(trackIds, ids)
                .filter((id) => entities[id]?.isComplete);
        }
    )
)
Example #3
Source File: selectors.ts    From jellyfin-audio-player with MIT License 6 votes vote down vote up
/**
 * Splits a set of albums into a list that is split by alphabet letters
 */
function splitAlbumsByAlphabet(state: AppState['music']['albums']): SectionedId[] {
    const { entities: albums } = state;
    const albumIds = albumsByArtist(state);
    const sections: SectionedId[] = ALPHABET_LETTERS.split('').map((l) => ({ label: l, data: [] }));

    albumIds.forEach((id) => {
        const album = albums[id];
        const letter = album?.AlbumArtist?.toUpperCase().charAt(0);
        const index = letter ? ALPHABET_LETTERS.indexOf(letter) : 26;
        (sections[index >= 0 ? index : 26].data as Array<EntityId>).push(id);
    });

    return sections;
}
Example #4
Source File: Playlists.tsx    From jellyfin-audio-player with MIT License 5 votes vote down vote up
Playlists: React.FC = () => {
    // Retrieve data from store
    const { entities, ids } = useTypedSelector((state) => state.music.playlists);
    const isLoading = useTypedSelector((state) => state.music.playlists.isLoading);
    const lastRefreshed = useTypedSelector((state) => state.music.playlists.lastRefreshed);
    
    // Initialise helpers
    const dispatch = useAppDispatch();
    const navigation = useNavigation<MusicNavigationProp>();
    const getImage = useGetImage();
    const listRef = useRef<FlatList<EntityId>>(null);

    const getItemLayout = useCallback((data: EntityId[] | null | undefined, index: number): { offset: number, length: number, index: number } => {
        const length = 220;
        const offset = length * index;
        return { index, length, offset };
    }, []);

    // Set callbacks
    const retrieveData = useCallback(() => dispatch(fetchAllPlaylists()), [dispatch]);
    const selectAlbum = useCallback((id: string) => {
        navigation.navigate('Playlist', { id });
    }, [navigation]);
    const generateItem: ListRenderItem<EntityId> = useCallback(({ item, index }) => {
        if (index % 2 === 1) {
            return <View key={item} />;
        }

        const nextItemId = ids[index + 1];
        const nextItem = entities[nextItemId];

        return (
            <View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item}>
                <GeneratedPlaylistItem
                    id={item}
                    imageUrl={getImage(item as string)}
                    name={entities[item]?.Name || ''}
                    onPress={selectAlbum}
                />
                {nextItem && 
                    <GeneratedPlaylistItem
                        id={nextItemId}
                        imageUrl={getImage(nextItemId as string)}
                        name={nextItem.Name || ''}
                        onPress={selectAlbum}
                    />
                }
            </View>
        );
    }, [entities, getImage, selectAlbum, ids]);

    // Retrieve data on mount
    useEffect(() => { 
        // GUARD: Only refresh this API call every set amounts of days
        if (!lastRefreshed || differenceInDays(lastRefreshed, new Date()) > PLAYLIST_CACHE_AMOUNT_OF_DAYS) {
            retrieveData(); 
        }
    });
    
    return (
        <FlatList
            data={ids} 
            refreshing={isLoading}
            onRefresh={retrieveData}
            getItemLayout={getItemLayout}
            ref={listRef}
            keyExtractor={(item, index) => `${item}_${index}`}
            renderItem={generateItem}
        />
    );
}
Example #5
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
queueTrackForDownload = createAction<EntityId>('download/queue')
Example #6
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
initializeDownload = createAction<{ id: EntityId, size?: number, jobId?: number }>('download/initialize')
Example #7
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
progressDownload = createAction<{ id: EntityId, progress: number, jobId?: number }>('download/progress')
Example #8
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
completeDownload = createAction<{ id: EntityId, location: string, size?: number }>('download/complete')
Example #9
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
failDownload = createAction<{ id: EntityId }>('download/fail')
Example #10
Source File: actions.ts    From jellyfin-audio-player with MIT License 5 votes vote down vote up
removeDownloadedTrack = createAsyncThunk(
    '/downloads/remove/track',
    async(id: EntityId) => {
        return unlink(`${DocumentDirectoryPath}/${id}.mp3`);
    }
)
Example #11
Source File: DownloadManager.ts    From jellyfin-audio-player with MIT License 4 votes vote down vote up
/**
 * 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 = useAppDispatch();
    
    // 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;
        }

        /**
         * 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', '');

                    // 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: file.size,
                    }));
                });
        }
        
        hydrateOrphanedDownloads();
        setHasRehydratedOrphans(true);
    }, [rehydrated, ids, hasRehydratedOrphans, dispatch]);


    return null;
}
Example #12
Source File: index.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
function Downloads() {
    const defaultStyles = useDefaultStyles();
    const dispatch = useAppDispatch();
    const getImage = useGetImage();

    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<number>((sum, id) => sum + (entities[id]?.size || 0), 0)
    ), [ids, entities]);

    /**
     * Handlers for actions in this components
     */

    // Delete a single downloaded track
    const handleDelete = useCallback((id: EntityId) => {
        dispatch(removeDownloadedTrack(id));
    }, [dispatch]);

    // Delete all downloaded tracks
    const handleDeleteAllTracks = useCallback(() => ids.forEach(handleDelete), [handleDelete, ids]);

    // Retry a single failed track
    const retryTrack = useCallback((id: EntityId) => {
        dispatch(queueTrackForDownload(id));
    }, [dispatch]);

    // Retry all failed tracks
    const failedIds = useMemo(() => ids.filter((id) => !entities[id]?.isComplete), [ids, entities]);
    const handleRetryFailed = useCallback(() => (
        failedIds.forEach(retryTrack)
    ), [failedIds, retryTrack]);

    /**
     * Render section
     */

    const ListHeaderComponent = useMemo(() => (
        <View style={[{ paddingHorizontal: 20, paddingBottom: 12, borderBottomWidth: 0.5 }, defaultStyles.border]}>
            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                <Text 
                    style={[
                        defaultStyles.textHalfOpacity,
                        { marginRight: 8, flex: 1, fontSize: 12 },
                    ]}
                    numberOfLines={1}
                >
                    {t('total-download-size')}: {formatBytes(totalDownloadSize)}
                </Text>
                <Button
                    icon={TrashIcon}
                    title={t('delete-all-tracks')}
                    onPress={handleDeleteAllTracks}
                    disabled={!ids.length}
                    size="small"
                />
            </View>
            {failedIds.length > 0 && (
                <Button
                    icon={ArrowClockwise}
                    title={t('retry-failed-downloads')}
                    onPress={handleRetryFailed}
                    disabled={failedIds.length === 0}
                    style={{ marginTop: 4 }}
                />
            )}
        </View>
    ), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]);
    
    const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => (
        <DownloadedTrack>
            <View style={{ marginRight: 12 }}>
                <ShadowWrapper size="small">
                    <AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} />
                </ShadowWrapper>
            </View>
            <View style={{ flexShrink: 1, marginRight: 8 }}>
                <Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
                    {tracks[item]?.Name}
                </Text>
                <Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
                    {tracks[item]?.AlbumArtist} {tracks[item]?.Album ? `— ${tracks[item]?.Album}` : ''}
                </Text>
            </View>
            <View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
                {entities[item]?.isComplete && entities[item]?.size ? (
                    <Text style={[defaultStyles.textQuarterOpacity, { marginRight: 12, fontSize: 12 }]}>
                        {formatBytes(entities[item]?.size || 0)}
                    </Text>
                ) : null}
                <View style={{ marginRight: 12 }}>
                    <DownloadIcon trackId={item} />
                </View>
                <Button onPress={() => handleDelete(item)} size="small" icon={TrashIcon} />
                {!entities[item]?.isComplete && (
                    <Button onPress={() => retryTrack(item)} size="small" icon={ArrowClockwise} style={{ marginLeft: 4 }} />
                )}
            </View>
        </DownloadedTrack>
    ), [entities, retryTrack, handleDelete, defaultStyles, tracks, getImage]);

    // If no tracks have been downloaded, show a short message describing this
    if (!ids.length) {
        return (
            <View style={{ margin: 24, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text style={[{ textAlign: 'center'}, defaultStyles.textHalfOpacity]}>
                    {t('no-downloads')}
                </Text>
            </View>
        );
    }

    return (
        <SafeAreaView style={{ flex: 1 }}>
            {ListHeaderComponent}
            <FlatList
                data={ids}
                style={{ flex: 1, paddingTop: 12 }}
                contentContainerStyle={{ flexGrow: 1 }}
                renderItem={renderItem}
            />
        </SafeAreaView>
    );
}
Example #13
Source File: Albums.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
Albums: React.FC = () => {
    // Retrieve data from store
    const { entities: albums } = useTypedSelector((state) => state.music.albums);
    const isLoading = useTypedSelector((state) => state.music.albums.isLoading);
    const lastRefreshed = useTypedSelector((state) => state.music.albums.lastRefreshed);
    const sections = useTypedSelector(selectAlbumsByAlphabet);
    
    // Initialise helpers
    const dispatch = useAppDispatch();
    const navigation = useNavigation<MusicNavigationProp>();
    const getImage = useGetImage();
    const listRef = useRef<SectionList<EntityId>>(null);

    const getItemLayout = useCallback((data: SectionedId[] | null, index: number): { offset: number, length: number, index: number } => {
        // We must wait for the ref to become available before we can use the
        // native item retriever in VirtualizedSectionList
        if (!listRef.current) {
            return { offset: 0, length: 0, index };
        }

        // Retrieve the right item info
        // @ts-ignore
        const wrapperListRef = (listRef.current?._wrapperListRef) as VirtualizedSectionList;
        const info: VirtualizedItemInfo = wrapperListRef._subExtractor(index);
        const { index: itemIndex, header, key } = info;
        const sectionIndex = parseInt(key.split(':')[0]);

        // We can then determine the "length" (=height) of this item. Header items
        // end up with an itemIndex of -1, thus are easy to identify.
        const length = header ? 50 : (itemIndex % 2 === 0 ? AlbumHeight : 0);
    
        // We'll also need to account for any unevenly-ended lists up until the
        // current item.
        const previousRows = data?.filter((row, i) => i < sectionIndex)
            .reduce((sum, row) => sum + Math.ceil(row.data.length / 2), 0) || 0;

        // We must also calcuate the offset, total distance from the top of the
        // screen. First off, we'll account for each sectionIndex that is shown up
        // until now. This only includes the heading for the current section if the
        // item is not the section header
        const headingOffset = HeadingHeight * (header ? sectionIndex : sectionIndex + 1);
        const currentRows = itemIndex > 1 ? Math.ceil((itemIndex + 1) / 2) : 0;
        const itemOffset = AlbumHeight * (previousRows + currentRows);
        const offset = headingOffset + itemOffset;
    
        return { index, length, offset };
    }, [listRef]);

    // Set callbacks
    const retrieveData = useCallback(() => dispatch(fetchAllAlbums()), [dispatch]);
    const selectAlbum = useCallback((id: string) => navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]);
    const selectLetter = useCallback((sectionIndex: number) => { 
        listRef.current?.scrollToLocation({ sectionIndex, itemIndex: 0, animated: false, }); 
    }, [listRef]);
    const generateItem = useCallback(({ item, index, section }: { item: EntityId, index: number, section: SectionedId }) => {
        if (index % 2 === 1) {
            return <View key={item} />;
        }
        const nextItem = section.data[index + 1];

        return (
            <View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item}>
                <GeneratedAlbumItem
                    id={item}
                    imageUrl={getImage(item as string)}
                    name={albums[item]?.Name || ''}
                    artist={albums[item]?.AlbumArtist || ''}
                    onPress={selectAlbum}
                />
                {albums[nextItem] && 
                    <GeneratedAlbumItem
                        id={nextItem}
                        imageUrl={getImage(nextItem as string)}
                        name={albums[nextItem]?.Name || ''}
                        artist={albums[nextItem]?.AlbumArtist || ''}
                        onPress={selectAlbum}
                    />
                }
            </View>
        );
    }, [albums, getImage, selectAlbum]);

    // Retrieve data on mount
    useEffect(() => { 
        // GUARD: Only refresh this API call every set amounts of days
        if (!lastRefreshed || differenceInDays(lastRefreshed, new Date()) > ALBUM_CACHE_AMOUNT_OF_DAYS) {
            retrieveData(); 
        }
    });
    
    return (
        <SafeAreaView>
            <AlphabetScroller onSelect={selectLetter} />
            <SectionList
                sections={sections} 
                refreshing={isLoading}
                onRefresh={retrieveData}
                getItemLayout={getItemLayout}
                ref={listRef}
                keyExtractor={(item) => item as string}
                renderSectionHeader={generateSection}
                renderItem={generateItem}
            />
        </SafeAreaView>
    );
}
Example #14
Source File: usePlayTracks.ts    From jellyfin-audio-player with MIT License 4 votes vote down vote up
/**
 * Generate a callback function that starts playing a full album given its
 * supplied id.
 */
export default function usePlayTracks() {
    const credentials = useTypedSelector(state => state.settings.jellyfin);
    const tracks = useTypedSelector(state => state.music.tracks.entities);
    const downloads = useTypedSelector(state => state.downloads.entities);

    return useCallback(async function playTracks(
        trackIds: EntityId[] | undefined,
        options: Partial<PlayOptions> = {},
    ): Promise<Track[] | undefined> {
        if (!trackIds) {
            return;
        }

        // Retrieve options and queue
        const {
            play,
            shuffle,
            method,
        } = Object.assign({}, defaults, options);
        const queue = await TrackPlayer.getQueue();

        // Convert all trackIds to the relevant format for react-native-track-player
        const generatedTracks = trackIds.map((trackId) => {
            const track = tracks[trackId];

            // GUARD: Check that the track actually exists in Redux
            if (!trackId || !track) {
                return;
            }

            // Retrieve the generated track from Jellyfin
            const generatedTrack = generateTrack(track, credentials);

            // Check if a downloaded version exists, and if so rewrite the URL
            const download = downloads[trackId];
            if (download?.location) {
                generatedTrack.url = 'file://' + download.location;
            }

            return generatedTrack;
        }).filter((t): t is Track => typeof t !== 'undefined');

        // Potentially shuffle all tracks
        const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;

        // Then, we'll need to check where to add the track
        switch(method) {
            case 'add-to-end': {
                await TrackPlayer.add(newTracks);

                // Then we'll skip to it and play it
                if (play) {
                    await TrackPlayer.skip((await TrackPlayer.getQueue()).length - newTracks.length);
                    await TrackPlayer.play();
                }

                break;
            }
            case 'add-after-currently-playing': {
                // Try and locate the current track
                const currentTrackIndex = await TrackPlayer.getCurrentTrack();
                
                // Since the argument is the id to insert the track BEFORE, we need
                // to get the current track + 1
                const targetTrack = currentTrackIndex >= 0 && queue.length > 1
                    ? queue[currentTrackIndex + 1].id
                    : undefined;
                
                // Depending on whether this track exists, we either add it there,
                // or at the end of the queue.
                await TrackPlayer.add(newTracks, targetTrack);
    
                if (play) {
                    await TrackPlayer.skip(currentTrackIndex + 1);
                    await TrackPlayer.play();
                }

                break;
            }
            case 'replace': {
                await TrackPlayer.reset();
                await TrackPlayer.add(newTracks);

                if (play) {
                    await TrackPlayer.play();
                }

                break;
            }
        }
    }, [credentials, downloads, tracks]);
}