@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 |
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 |
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 |
/**
* 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 |
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 |
queueTrackForDownload = createAction<EntityId>('download/queue')
Example #6
Source File: actions.ts From jellyfin-audio-player with MIT License | 5 votes |
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 |
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 |
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 |
failDownload = createAction<{ id: EntityId }>('download/fail')
Example #10
Source File: actions.ts From jellyfin-audio-player with MIT License | 5 votes |
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 |
/**
* 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 |
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 |
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 |
/**
* 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]);
}