Add home screen state persistence
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
79
src/state/home.ts
Normal file
79
src/state/home.ts
Normal 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
61
src/state/index.ts
Normal 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
114
src/state/settings.ts
Normal 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;
|
Reference in New Issue
Block a user