Add home screen state persistence

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-11 21:29:49 +03:00
parent 99195fe481
commit 5518fba787
11 changed files with 154 additions and 70 deletions

79
src/state/home.ts Normal file
View File

@@ -0,0 +1,79 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SORT, SORT_DIRECTION, VIEW } from '../types';
interface HomeState {
sort: SORT;
sortDirection: SORT_DIRECTION;
view: VIEW;
favoritesOnly: boolean;
}
const initialState: HomeState = {
sort: SORT.TITLE,
sortDirection: SORT_DIRECTION.ASCENDING,
view: VIEW.MASONRY,
favoritesOnly: false,
};
const homeSlice = createSlice({
name: 'home',
initialState,
reducers: {
setSort: (state, action: PayloadAction<SORT>) => {
state.sort = action.payload;
},
setSortDirection: (state, action: PayloadAction<SORT_DIRECTION>) => {
state.sortDirection = action.payload;
},
toggleSortDirection: state => {
state.sortDirection ^= 1;
},
setView: (state, action: PayloadAction<VIEW>) => {
state.view = action.payload;
},
cycleView: state => {
switch (state.view) {
case VIEW.MASONRY: {
state.view = VIEW.LIST;
break;
}
case VIEW.LIST: {
state.view = VIEW.GRID;
break;
}
case VIEW.GRID: {
state.view = VIEW.MASONRY;
break;
}
}
},
setFavoritesOnly: (state, action: PayloadAction<boolean>) => {
state.favoritesOnly = action.payload;
},
toggleFavoritesOnly: state => {
state.favoritesOnly = !state.favoritesOnly;
},
},
});
const {
setSort,
setSortDirection,
toggleSortDirection,
setView,
cycleView,
setFavoritesOnly,
toggleFavoritesOnly,
} = homeSlice.actions;
export {
type HomeState,
setSort,
setSortDirection,
toggleSortDirection,
setView,
cycleView,
setFavoritesOnly,
toggleFavoritesOnly,
};
export default homeSlice.reducer;

61
src/state/index.ts Normal file
View File

@@ -0,0 +1,61 @@
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import {
persistReducer,
persistStore,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist';
import { createRealmPersistStorage } from '@bankify/redux-persist-realm';
import settingsReducer from './settings';
import homeReducer from './home';
const rootReducer = combineReducers({
settings: settingsReducer,
home: homeReducer,
});
interface RootState {
settings: ReturnType<typeof settingsReducer>;
home: ReturnType<typeof homeReducer>;
}
const persistConfig = {
key: 'root',
storage: createRealmPersistStorage({ path: 'redux.realm' }),
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
const persistor = persistStore(store);
export { type RootState, store, persistor };
export {
type SettingsState,
updateStorageUri,
updateNoMedia,
validateSettings,
} from './settings';
export {
type HomeState,
setSort,
setSortDirection,
toggleSortDirection,
setView,
cycleView,
setFavoritesOnly,
toggleFavoritesOnly,
} from './home';

114
src/state/settings.ts Normal file
View File

@@ -0,0 +1,114 @@
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import {
createFile,
deleteFile,
getPersistedUriPermissions,
} from 'react-native-scoped-storage';
import { FileSystem, AndroidScoped } from 'react-native-file-access';
import { clearPermissions, isPermissionForPath } from '../utilities';
import { RootState } from '.';
interface SettingsState {
storageUri: string | undefined;
noMedia: boolean;
}
const initialState: SettingsState = {
storageUri: undefined,
noMedia: false,
};
const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {
setStorageUri: (state, action: PayloadAction<string | undefined>) => {
state.storageUri = action.payload;
},
setNoMedia: (state, action: PayloadAction<boolean>) => {
state.noMedia = action.payload;
},
},
});
const { setStorageUri, setNoMedia } = settingsSlice.actions;
const updateStorageUri = createAsyncThunk(
'settings/updateStorageUri',
async (newStorageUri: string, { dispatch }) => {
await clearPermissions([newStorageUri]);
dispatch(setStorageUri(newStorageUri));
},
);
const updateNoMedia = createAsyncThunk(
'settings/updateNoMedia',
async (newNoMedia: boolean, { dispatch, getState }) => {
const state = getState() as RootState;
const { storageUri } = state.settings;
if (!storageUri) return;
const noMediaExists = await FileSystem.exists(
AndroidScoped.appendPath(storageUri, '.nomedia'),
);
if (newNoMedia && !noMediaExists) {
await createFile(storageUri, '.nomedia', 'text/x-unknown');
} else if (!newNoMedia && noMediaExists) {
await deleteFile(AndroidScoped.appendPath(storageUri, '.nomedia'));
}
dispatch(setNoMedia(newNoMedia));
},
);
const validateSettings = createAsyncThunk(
'settings/validateSettings',
// eslint-disable-next-line @typescript-eslint/naming-convention
async (_, { dispatch, getState }) => {
const state = getState() as RootState;
const { storageUri, noMedia } = state.settings;
if (!storageUri) {
return;
}
const permissions = await getPersistedUriPermissions();
if (
!permissions.some(permission =>
isPermissionForPath(permission, storageUri),
)
) {
dispatch(setStorageUri());
return;
}
try {
const exists = await FileSystem.exists(storageUri);
if (!exists) {
throw new Error('Storage URI does not exist');
}
const isDirectory = await FileSystem.isDir(storageUri);
if (!isDirectory) {
throw new Error('Storage URI is not a directory');
}
} catch {
dispatch(setStorageUri());
return;
}
const isNoMedia = await FileSystem.exists(
AndroidScoped.appendPath(storageUri, '.nomedia'),
);
if (noMedia !== isNoMedia) {
dispatch(setNoMedia(isNoMedia));
}
},
);
export {
type SettingsState,
updateStorageUri,
updateNoMedia,
validateSettings,
};
export default settingsSlice.reducer;