Sfoglia il codice sorgente

Functionality to delete and edit custom themes

Paweł Malak 3 anni fa
parent
commit
668edb03d3

+ 3 - 0
CHANGELOG.md

@@ -1,3 +1,6 @@
+### v2.3.0 (TBA)
+- Added custom theme editor ([#246](https://github.com/pawelmalak/flame/issues/246))
+
 ### v2.2.2 (2022-03-21)
 - Added option to get user location directly from the app ([#287](https://github.com/pawelmalak/flame/issues/287))
 - Fixed bug with local search not working when using prefix ([#289](https://github.com/pawelmalak/flame/issues/289))

+ 1 - 1
README.md

@@ -11,7 +11,7 @@ Flame is self-hosted startpage for your server. Its design is inspired (heavily)
 - 📌 Pin your favourite items to the homescreen for quick and easy access
 - 🔍 Integrated search bar with local filtering, 11 web search providers and ability to add your own
 - 🔑 Authentication system to protect your settings, apps and bookmarks
-- 🔨 Dozens of options to customize Flame interface to your needs, including support for custom CSS and 15 built-in color themes
+- 🔨 Dozens of options to customize Flame interface to your needs, including support for custom CSS, 15 built-in color themes and custom theme builder
 - ☀️ Weather widget with current temperature, cloud coverage and animated weather status
 - 🐳 Docker integration to automatically pick and add apps based on their labels
 

+ 15 - 2
client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx

@@ -1,7 +1,9 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 
 // Redux
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { actionCreators } from '../../../../store';
 import { State } from '../../../../store/reducers';
 
 // Other
@@ -21,11 +23,21 @@ interface Props {
 export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
   const {
     auth: { isAuthenticated },
+    theme: { themeInEdit },
   } = useSelector((state: State) => state);
 
+  const { editTheme } = bindActionCreators(actionCreators, useDispatch());
+
   const [showModal, toggleShowModal] = useState(false);
   const [isInEdit, toggleIsInEdit] = useState(false);
 
+  useEffect(() => {
+    if (themeInEdit) {
+      toggleIsInEdit(false);
+      toggleShowModal(true);
+    }
+  }, [themeInEdit]);
+
   return (
     <div className={classes.ThemeBuilder}>
       {/* MODALS */}
@@ -45,6 +57,7 @@ export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
         <div className={classes.Buttons}>
           <Button
             click={() => {
+              editTheme(null);
               toggleIsInEdit(false);
               toggleShowModal(!showModal);
             }}

+ 21 - 5
client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.tsx

@@ -19,10 +19,13 @@ interface Props {
 
 export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
   const {
-    theme: { activeTheme },
+    theme: { activeTheme, themeInEdit },
   } = useSelector((state: State) => state);
 
-  const { addTheme } = bindActionCreators(actionCreators, useDispatch());
+  const { addTheme, updateTheme } = bindActionCreators(
+    actionCreators,
+    useDispatch()
+  );
 
   const [formData, setFormData] = useState<Theme>({
     name: '',
@@ -38,6 +41,12 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
     setFormData({ ...formData, colors: activeTheme.colors });
   }, [activeTheme]);
 
+  useEffect(() => {
+    if (themeInEdit) {
+      setFormData(themeInEdit);
+    }
+  }, [themeInEdit]);
+
   const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
     const { name, value } = e.target;
 
@@ -62,8 +71,11 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
   const formHandler = (e: FormEvent) => {
     e.preventDefault();
 
-    // add new theme
-    addTheme(formData);
+    if (!themeInEdit) {
+      addTheme(formData);
+    } else {
+      updateTheme(formData);
+    }
 
     // close modal
     modalHandler();
@@ -125,7 +137,11 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
         </InputGroup>
       </div>
 
-      <Button>Add theme</Button>
+      {!themeInEdit ? (
+        <Button>Add theme</Button>
+      ) : (
+        <Button>Update theme</Button>
+      )}
     </ModalForm>
   );
 };

+ 8 - 2
client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx

@@ -19,9 +19,15 @@ export const ThemeEditor = (props: Props): JSX.Element => {
     theme: { userThemes },
   } = useSelector((state: State) => state);
 
-  const { deleteTheme } = bindActionCreators(actionCreators, useDispatch());
+  const { deleteTheme, editTheme } = bindActionCreators(
+    actionCreators,
+    useDispatch()
+  );
 
-  const updateHandler = (theme: Theme) => {};
+  const updateHandler = (theme: Theme) => {
+    props.modalHandler();
+    editTheme(theme);
+  };
 
   const deleteHandler = (theme: Theme) => {
     if (window.confirm(`Are you sure you want to delete this theme?`)) {

+ 54 - 0
client/src/store/action-creators/theme.ts

@@ -1,8 +1,11 @@
 import { Dispatch } from 'redux';
 import {
   AddThemeAction,
+  DeleteThemeAction,
+  EditThemeAction,
   FetchThemesAction,
   SetThemeAction,
+  UpdateThemeAction,
 } from '../actions/theme';
 import { ActionType } from '../action-types';
 import { Theme, ApiResponse, ThemeColors } from '../../interfaces';
@@ -71,3 +74,54 @@ export const addTheme =
       });
     }
   };
+
+export const deleteTheme =
+  (name: string) => async (dispatch: Dispatch<DeleteThemeAction>) => {
+    try {
+      const res = await axios.delete<ApiResponse<Theme[]>>(
+        `/api/themes/${name}`,
+        { headers: applyAuth() }
+      );
+
+      dispatch({
+        type: ActionType.deleteTheme,
+        payload: res.data.data,
+      });
+
+      dispatch<any>({
+        type: ActionType.createNotification,
+        payload: {
+          title: 'Success',
+          message: 'Theme deleted',
+        },
+      });
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+export const editTheme =
+  (theme: Theme | null) => (dispatch: Dispatch<EditThemeAction>) => {
+    dispatch({
+      type: ActionType.editTheme,
+      payload: theme,
+    });
+  };
+
+export const updateTheme =
+  (theme: Theme) => async (dispatch: Dispatch<UpdateThemeAction>) => {
+    try {
+      const res = await axios.put<ApiResponse<Theme[]>>(
+        `/api/themes/${theme.name}`,
+        theme,
+        { headers: applyAuth() }
+      );
+
+      dispatch({
+        type: ActionType.updateTheme,
+        payload: res.data.data,
+      });
+    } catch (err) {
+      console.log(err);
+    }
+  };

+ 3 - 0
client/src/store/action-types/index.ts

@@ -3,6 +3,9 @@ export enum ActionType {
   setTheme = 'SET_THEME',
   fetchThemes = 'FETCH_THEMES',
   addTheme = 'ADD_THEME',
+  deleteTheme = 'DELETE_THEME',
+  updateTheme = 'UPDATE_THEME',
+  editTheme = 'EDIT_THEME',
   // CONFIG
   getConfig = 'GET_CONFIG',
   updateConfig = 'UPDATE_CONFIG',

+ 13 - 1
client/src/store/actions/index.ts

@@ -1,6 +1,13 @@
 import { App } from '../../interfaces';
 
-import { SetThemeAction } from './theme';
+import {
+  AddThemeAction,
+  DeleteThemeAction,
+  EditThemeAction,
+  FetchThemesAction,
+  SetThemeAction,
+  UpdateThemeAction,
+} from './theme';
 
 import {
   AddQueryAction,
@@ -54,6 +61,11 @@ import {
 export type Action =
   // Theme
   | SetThemeAction
+  | FetchThemesAction
+  | AddThemeAction
+  | DeleteThemeAction
+  | UpdateThemeAction
+  | EditThemeAction
   // Config
   | GetConfigAction
   | UpdateConfigAction

+ 15 - 0
client/src/store/actions/theme.ts

@@ -15,3 +15,18 @@ export interface AddThemeAction {
   type: ActionType.addTheme;
   payload: Theme;
 }
+
+export interface DeleteThemeAction {
+  type: ActionType.deleteTheme;
+  payload: Theme[];
+}
+
+export interface UpdateThemeAction {
+  type: ActionType.updateTheme;
+  payload: Theme[];
+}
+
+export interface EditThemeAction {
+  type: ActionType.editTheme;
+  payload: Theme | null;
+}

+ 24 - 1
client/src/store/reducers/theme.ts

@@ -1,12 +1,13 @@
 import { Action } from '../actions';
 import { ActionType } from '../action-types';
-import { Theme, ThemeColors } from '../../interfaces/Theme';
+import { Theme } from '../../interfaces/Theme';
 import { arrayPartition, parsePABToTheme } from '../../utility';
 
 interface ThemeState {
   activeTheme: Theme;
   themes: Theme[];
   userThemes: Theme[];
+  themeInEdit: Theme | null;
 }
 
 const savedTheme = localStorage.theme
@@ -23,6 +24,7 @@ const initialState: ThemeState = {
   },
   themes: [],
   userThemes: [],
+  themeInEdit: null,
 };
 
 export const themeReducer = (
@@ -60,6 +62,27 @@ export const themeReducer = (
       };
     }
 
+    case ActionType.deleteTheme: {
+      return {
+        ...state,
+        userThemes: action.payload,
+      };
+    }
+
+    case ActionType.editTheme: {
+      return {
+        ...state,
+        themeInEdit: action.payload,
+      };
+    }
+
+    case ActionType.updateTheme: {
+      return {
+        ...state,
+        userThemes: action.payload,
+      };
+    }
+
     default:
       return state;
   }