Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "SimpleMediaBrowser",
"owner": "kallb123",
"slug": "SimpleMediaBrowser",
"version": "1.11.1",
"version": "1.11.2",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "simplemediabrowser",
Expand All @@ -20,7 +20,7 @@
},
"predictiveBackGestureEnabled": false,
"package": "net.nawt.simplemediabrowser",
"versionCode": 45
"versionCode": 46
},
"web": {
"output": "static",
Expand Down
134 changes: 131 additions & 3 deletions src/components/MediaBrowserScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { BackHandler, StyleSheet, TouchableOpacity, View, useWindowDimensions } from 'react-native';
import { Alert, BackHandler, StyleSheet, TouchableOpacity, View, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { openMediaInExternalApp } from '@/scripts/openMedia';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useThemeColor } from '@/hooks/useThemeColor';
import { File } from 'expo-file-system';
import type { VideoThumbnail } from 'expo-video';
import { Link, router } from 'expo-router';
import { useEffect, useMemo, useCallback, useState, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectMediaSources, selectMediaStructure, selectViewScale, selectViewOrientation } from '@/store/settingsReducer';
import { setMediaOverride } from '@/store/libraryReducer';
import { clearEpisodeMetadata, clearMovieMetadata, clearMediaOverride, clearShowMetadata, setMediaOverride } from '@/store/libraryReducer';

Check warning on line 13 in src/components/MediaBrowserScreen.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

'/home/runner/work/SimpleMediaBrowser/SimpleMediaBrowser/src/store/libraryReducer.ts' imported multiple times
import { selectMediaLibrary, selectMovies, selectIsScanning, selectMediaOverrides, selectScanProgress } from '@/store/libraryReducer';

Check warning on line 14 in src/components/MediaBrowserScreen.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

'/home/runner/work/SimpleMediaBrowser/SimpleMediaBrowser/src/store/libraryReducer.ts' imported multiple times
import { MetadataService } from '@/scripts/MetadataService';
import { store } from '@/store/store';
import { FlashList, FlashListRef } from '@shopify/flash-list';
import { IMediaObject, thumbnailCache } from '@/scripts/FileScanner';
import type { IMediaLibrary } from '@/store/libraryReducer';

Check warning on line 19 in src/components/MediaBrowserScreen.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

'/home/runner/work/SimpleMediaBrowser/SimpleMediaBrowser/src/store/libraryReducer.ts' imported multiple times
import type { IMediaOverride } from '@/store/libraryReducer';
import type { viewTypes } from '@/store/settingsReducer';
import { logger } from '@/scripts/Logger';
Expand Down Expand Up @@ -635,13 +638,121 @@

// Derive the selected show names (without prefix) for the Merge action.
const selectedShowKeys = useMemo(
() => Array.from(selectedItems).filter((k) => k.startsWith('show:')).map((k) => k.slice(5)),
() => (Array.from(selectedItems) as string[]).filter((k) => k.startsWith('show:')).map((k) => k.slice(5)),
[selectedItems],
);

const selectedMoviePaths = useMemo(
() => (Array.from(selectedItems) as string[]).filter((k) => k.startsWith('movie:')).map((k) => k.slice(6)),
[selectedItems],
);

const selectedEpisodePaths = useMemo(
() => (Array.from(selectedItems) as string[]).filter((k) => k.startsWith('episode:')).map((k) => k.slice(8)),
[selectedItems],
);

const shouldShowToolbar = editMode && selectedItems.size >= 1;
const shouldShowMerge = selectedShowKeys.length >= 2;

const deleteLocalFile = useCallback((uri?: string) => {
if (!uri) return;
try {
const file = new File(uri);
if (file.exists) {
file.delete();
}
} catch {
// ignore failures; refresh can still proceed
}
}, []);

const performResetSelectedMetadata = useCallback(async () => {
const selectedKeys = Array.from(selectedItems);
if (selectedKeys.length === 0) return;

const selectedShows = selectedShowKeys;
const selectedMoviesForRefresh = selectedMoviePaths;
const selectedEpisodes = selectedEpisodePaths;

// Clear selected overrides first, then clear metadata state and downloaded files.
for (const key of selectedKeys) {
dispatch(clearMediaOverride(key));
}

const state = store.getState();
for (const showName of selectedShows) {
const show = state.libraryReducer.mediaLibrary[showName];
if (show?.poster) {
deleteLocalFile(show.poster);
}
for (const season of Object.values(show?.seasons ?? {})) {
for (const ep of Object.values(season.episodes)) {
deleteLocalFile(ep.tmdbThumbnail);
}
}
dispatch(clearShowMetadata(showName));
}

for (const moviePath of selectedMoviesForRefresh) {
const movie = state.libraryReducer.movies.find((m: IMediaObject) => m.path === moviePath);
if (movie?.poster) {
deleteLocalFile(movie.poster);
}
dispatch(clearMovieMetadata(moviePath));
}

for (const episodePath of selectedEpisodes) {
const episode = Object.values(state.libraryReducer.mediaLibrary).flatMap((show) =>
Object.values(show.seasons).flatMap((season) => Object.values(season.episodes)),
).find((ep) => ep.path === episodePath);
if (episode?.tmdbThumbnail) {
deleteLocalFile(episode.tmdbThumbnail);
}
dispatch(clearEpisodeMetadata(episodePath));
}

clearItemSelection();

// Re-read the refreshed state for the selected subset before metadata enrichment.
const refreshedState = store.getState();
const librarySubset: IMediaLibrary = {};
for (const showName of selectedShows) {
const show = refreshedState.libraryReducer.mediaLibrary[showName];
if (show) librarySubset[showName] = show;
}
for (const episodePath of selectedEpisodes) {
const showEntry = Object.entries(refreshedState.libraryReducer.mediaLibrary).find(([, show]) =>
Object.values(show.seasons).some((season) =>
Object.values(season.episodes).some((ep) => ep.path === episodePath),
),
);
if (showEntry) {
const [showName, show] = showEntry;
librarySubset[showName] = show;
}
}
const moviesSubset = refreshedState.libraryReducer.movies.filter((movie: IMediaObject) => selectedMoviesForRefresh.includes(movie.parsedPath));

try {
await MetadataService.getInstance().enrichAll(librarySubset, moviesSubset);
} catch (e) {
logger.warn('MediaBrowserScreen', 'Failed to refresh metadata for selected items', e);
}
}, [dispatch, selectedItems, selectedShowKeys, selectedMoviePaths, selectedEpisodePaths, clearItemSelection, deleteLocalFile]);

const handleResetMetadata = useCallback(() => {
if (selectedItems.size === 0) return;
Alert.alert(
'Reset metadata',
'This will remove overrides and downloaded metadata for the selected items, then refresh their metadata from the configured provider.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Reset', style: 'destructive', onPress: () => { void performResetSelectedMetadata(); } },
],
);
}, [selectedItems.size, performResetSelectedMetadata]);

/**
* Marks all currently selected items as hidden by setting `hidden: true` in their
* `mediaOverrides` entry. No files are deleted; the action is fully reversible by
Expand Down Expand Up @@ -838,6 +949,12 @@
>
<ThemedText style={styles.hideButtonText}>Hide</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={styles.resetButton}
onPress={handleResetMetadata}
>
<ThemedText style={styles.resetButtonText}>Reset</ThemedText>
</TouchableOpacity>
{shouldShowMerge && (
<TouchableOpacity
style={styles.mergeButton}
Expand Down Expand Up @@ -1092,6 +1209,17 @@
fontWeight: '600',
fontSize: 13,
},
resetButton: {
backgroundColor: '#6a5acd',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 8,
},
resetButtonText: {
color: '#fff',
fontWeight: '600',
fontSize: 13,
},
mergeButton: {
backgroundColor: '#0a7ea4',
borderRadius: 8,
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/EditModeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const EditModeContext = createContext<EditModeContextType>({
setEditMode: () => {},
drawerUnlocked: false,
setDrawerUnlocked: () => {},
selectedItems: new Set(),
selectedItems: new Set<string>(),
toggleItemSelection: () => {},
clearItemSelection: () => {},
});
Expand Down
40 changes: 34 additions & 6 deletions src/store/libraryReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,34 @@ export const settingsSlice = createSlice({
clearMediaOverride: (state, action: PayloadAction<string>) => {
delete state.mediaOverrides[action.payload];
},
clearShowMetadata: (state, action: PayloadAction<string>) => {
const show = state.mediaLibrary[action.payload];
if (!show) return;
show.poster = '';
for (const season of Object.values(show.seasons) as IMediaSeason[]) {
for (const ep of Object.values(season.episodes) as IMediaObject[]) {
ep.tmdbTitle = undefined;
ep.tmdbThumbnail = undefined;
}
}
},
clearMovieMetadata: (state, action: PayloadAction<string>) => {
const movie = state.movies.find((m) => m.path === action.payload);
if (!movie) return;
movie.poster = '';
},
clearEpisodeMetadata: (state, action: PayloadAction<string>) => {
for (const show of Object.values(state.mediaLibrary) as IMediaShow[]) {
for (const season of Object.values(show.seasons) as IMediaSeason[]) {
const episode = Object.values(season.episodes as { [key: string]: IMediaObject }).find((ep) => ep.path === action.payload);
if (episode) {
episode.tmdbTitle = undefined;
episode.tmdbThumbnail = undefined;
return;
}
}
}
},
/**
* Clears both the TV library and the movie list. Dispatched at the start
* of each scan so stale data is not shown before streaming results arrive.
Expand Down Expand Up @@ -419,8 +447,8 @@ export const settingsSlice = createSlice({
clearShowEpisodeMetadata: (state, action: PayloadAction<string>) => {
const show = state.mediaLibrary[action.payload];
if (!show) return;
for (const season of Object.values(show.seasons)) {
for (const ep of Object.values(season.episodes)) {
for (const season of Object.values(show.seasons) as IMediaSeason[]) {
for (const ep of Object.values(season.episodes) as IMediaObject[]) {
ep.tmdbTitle = undefined;
ep.tmdbThumbnail = undefined;
}
Expand All @@ -437,10 +465,10 @@ export const settingsSlice = createSlice({
for (const key of Object.keys(state.mediaOverrides)) {
delete state.mediaOverrides[key].poster;
}
for (const show of Object.values(state.mediaLibrary)) {
for (const show of Object.values(state.mediaLibrary) as IMediaShow[]) {
show.poster = '';
for (const season of Object.values(show.seasons)) {
for (const ep of Object.values(season.episodes)) {
for (const season of Object.values(show.seasons) as IMediaSeason[]) {
for (const ep of Object.values(season.episodes) as IMediaObject[]) {
ep.tmdbThumbnail = undefined;
}
}
Expand Down Expand Up @@ -494,7 +522,7 @@ export const settingsSlice = createSlice({
},
})

export const { addToScanList, setScanList, clearScanList, setMediaLibrary, setMovies, setIsScanning, setScanProgress, setThumbnail, clearThumbnails, updateShowMetadata, updateMovieMetadata, setMediaOverride, clearMediaOverride, clearLibraryAndMovies, mergeEpisodeBatch, appendMovieBatch, updateShowPoster, setMoviePoster, mergeDuplicateShows, clearPosterOverrides, clearTvdbPosterOverrides, clearAllOverrides, updateSeasonEpisodeMetadata, clearShowEpisodeMetadata } = settingsSlice.actions;
export const { addToScanList, setScanList, clearScanList, setMediaLibrary, setMovies, setIsScanning, setScanProgress, setThumbnail, clearThumbnails, updateShowMetadata, updateMovieMetadata, setMediaOverride, clearMediaOverride, clearShowMetadata, clearMovieMetadata, clearEpisodeMetadata, clearLibraryAndMovies, mergeEpisodeBatch, appendMovieBatch, updateShowPoster, setMoviePoster, mergeDuplicateShows, clearPosterOverrides, clearTvdbPosterOverrides, clearAllOverrides, updateSeasonEpisodeMetadata, clearShowEpisodeMetadata } = settingsSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const selectScanList = (state: RootState) => state.libraryReducer.scanList;
Expand Down
Loading