diff --git a/.dev/build_dev.sh b/.dev/build_dev.sh new file mode 100644 index 0000000..6c0bd26 --- /dev/null +++ b/.dev/build_dev.sh @@ -0,0 +1 @@ +docker build -t flame:dev -f .docker/Dockerfile . \ No newline at end of file diff --git a/.dev/build_latest.sh b/.dev/build_latest.sh index 9621433..47916fb 100644 --- a/.dev/build_latest.sh +++ b/.dev/build_latest.sh @@ -1,2 +1,2 @@ -docker build -t pawelmalak/flame -t "pawelmalak/flame:$1" -f .docker/Dockerfile "$2" \ +docker build -t pawelmalak/flame -t "pawelmalak/flame:$1" -f .docker/Dockerfile . \ && docker push pawelmalak/flame && docker push "pawelmalak/flame:$1" \ No newline at end of file diff --git a/.dev/build_multiarch.sh b/.dev/build_multiarch.sh index ef1e10c..6ebe89c 100644 --- a/.dev/build_multiarch.sh +++ b/.dev/build_multiarch.sh @@ -3,4 +3,4 @@ docker buildx build \ -f .docker/Dockerfile.multiarch \ -t pawelmalak/flame:multiarch \ -t "pawelmalak/flame:multiarch$1" \ - --push "$2" \ No newline at end of file + --push . \ No newline at end of file diff --git a/.env b/.env index 887f01e..99ccaf9 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ PORT=5005 NODE_ENV=development -VERSION=2.2.2 +VERSION=2.3.0 PASSWORD=flame_password SECRET=e02eb43d69953658c6d07311d6313f2d4467672cb881f96b29368ba1f3f4da4b \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d3e1d..86e876f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### v2.3.0 (2022-03-25) +- Added custom theme editor ([#246](https://github.com/pawelmalak/flame/issues/246)) +- Added option to set secondary search provider ([#295](https://github.com/pawelmalak/flame/issues/295)) +- Fixed bug where pressing Enter with empty search bar would redirect to search results ([#325](https://github.com/pawelmalak/flame/issues/325)) +- Fixed bug where user could create empty app or bookmark which was causing page to go blank ([#332](https://github.com/pawelmalak/flame/issues/332)) +- Added new theme: Mint + ### 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)) diff --git a/README.md b/README.md index 290f230..a8424c9 100644 --- a/README.md +++ b/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 diff --git a/api.js b/api.js index 840529a..45be359 100644 --- a/api.js +++ b/api.js @@ -22,6 +22,7 @@ api.use('/api/categories', require('./routes/category')); api.use('/api/bookmarks', require('./routes/bookmark')); api.use('/api/queries', require('./routes/queries')); api.use('/api/auth', require('./routes/auth')); +api.use('/api/themes', require('./routes/themes')); // Custom error handler api.use(errorHandler); diff --git a/client/.env b/client/.env index f31a910..e8597c1 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=2.2.2 \ No newline at end of file +REACT_APP_VERSION=2.3.0 \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 68faaea..e302c57 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,7 +10,7 @@ import { actionCreators, store } from './store'; import { State } from './store/reducers'; // Utils -import { checkVersion, decodeToken } from './utility'; +import { checkVersion, decodeToken, parsePABToTheme } from './utility'; // Routes import { Home } from './components/Home/Home'; @@ -31,7 +31,7 @@ export const App = (): JSX.Element => { const { config, loading } = useSelector((state: State) => state.config); const dispath = useDispatch(); - const { fetchQueries, setTheme, logout, createNotification } = + const { fetchQueries, setTheme, logout, createNotification, fetchThemes } = bindActionCreators(actionCreators, dispath); useEffect(() => { @@ -51,9 +51,12 @@ export const App = (): JSX.Element => { } }, 1000); + // load themes + fetchThemes(); + // set user theme if present if (localStorage.theme) { - setTheme(localStorage.theme); + setTheme(parsePABToTheme(localStorage.theme)); } // check for updated @@ -68,7 +71,7 @@ export const App = (): JSX.Element => { // If there is no user theme, set the default one useEffect(() => { if (!loading && !localStorage.theme) { - setTheme(config.defaultTheme, false); + setTheme(parsePABToTheme(config.defaultTheme), false); } }, [loading]); diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index a751ad6..a154e0a 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -18,10 +18,8 @@ export const AppForm = ({ modalHandler }: Props): JSX.Element => { const { appInUpdate } = useSelector((state: State) => state.apps); const dispatch = useDispatch(); - const { addApp, updateApp, setEditApp } = bindActionCreators( - actionCreators, - dispatch - ); + const { addApp, updateApp, setEditApp, createNotification } = + bindActionCreators(actionCreators, dispatch); const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); @@ -58,6 +56,17 @@ export const AppForm = ({ modalHandler }: Props): JSX.Element => { const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); + for (let field of ['name', 'url', 'icon'] as const) { + if (/^ +$/.test(formData[field])) { + createNotification({ + title: 'Error', + message: `Field cannot be empty: ${field}`, + }); + + return; + } + } + const createFormData = (): FormData => { const data = new FormData(); diff --git a/client/src/components/Bookmarks/Form/BookmarksForm.tsx b/client/src/components/Bookmarks/Form/BookmarksForm.tsx index 893b334..9c1b728 100644 --- a/client/src/components/Bookmarks/Form/BookmarksForm.tsx +++ b/client/src/components/Bookmarks/Form/BookmarksForm.tsx @@ -69,6 +69,17 @@ export const BookmarksForm = ({ const formSubmitHandler = (e: FormEvent): void => { e.preventDefault(); + for (let field of ['name', 'url', 'icon'] as const) { + if (/^ +$/.test(formData[field])) { + createNotification({ + title: 'Error', + message: `Field cannot be empty: ${field}`, + }); + + return; + } + } + const createFormData = (): FormData => { const data = new FormData(); if (customIcon) { diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 9920073..f0b78a5 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -64,16 +64,22 @@ export const SearchBar = (props: Props): JSX.Element => { }; const searchHandler = (e: KeyboardEvent) => { - const { isLocal, search, query, isURL, sameTab } = searchParser( - inputRef.current.value - ); + const { + isLocal, + encodedURL, + primarySearch, + secondarySearch, + isURL, + sameTab, + rawQuery, + } = searchParser(inputRef.current.value); if (isLocal) { - setLocalSearch(search); + setLocalSearch(encodedURL); } if (e.code === 'Enter' || e.code === 'NumpadEnter') { - if (!query.prefix) { + if (!primarySearch.prefix) { // Prefix not found -> emit notification createNotification({ title: 'Error', @@ -90,19 +96,21 @@ export const SearchBar = (props: Props): JSX.Element => { } else if (bookmarkSearchResult?.[0]?.bookmarks?.length) { redirectUrl(bookmarkSearchResult[0].bookmarks[0].url, sameTab); } else { - // no local results -> search the internet with the default search provider - let template = query.template; + // no local results -> search the internet with the default search provider if query is not empty + if (!/^ *$/.test(rawQuery)) { + let template = primarySearch.template; - if (query.prefix === 'l') { - template = 'https://duckduckgo.com/?q='; + if (primarySearch.prefix === 'l') { + template = secondarySearch.template; + } + + const url = `${template}${encodedURL}`; + redirectUrl(url, sameTab); } - - const url = `${template}${search}`; - redirectUrl(url, sameTab); } } else { // Valid query -> redirect to search results - const url = `${query.template}${search}`; + const url = `${primarySearch.template}${encodedURL}`; redirectUrl(url, sameTab); } } else if (e.code === 'Escape') { diff --git a/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.module.css b/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.module.css deleted file mode 100644 index 73297cc..0000000 --- a/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.QueriesGrid { - display: grid; - grid-template-columns: repeat(3, 1fr); -} - -.QueriesGrid span { - color: var(--color-primary); -} - -.QueriesGrid span:last-child { - margin-bottom: 10px; -} - -.ActionIcons { - display: flex; -} - -.ActionIcons svg { - width: 20px; -} - -.ActionIcons svg:hover { - cursor: pointer; -} - -.Separator { - grid-column: 1 / 4; - border-bottom: 1px solid var(--color-primary); - margin: 10px 0; -} diff --git a/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.tsx b/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.tsx index 747be3b..8471fae 100644 --- a/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.tsx +++ b/client/src/components/Settings/GeneralSettings/CustomQueries/CustomQueries.tsx @@ -9,11 +9,8 @@ import { actionCreators } from '../../../../store'; // Typescript import { Query } from '../../../../interfaces'; -// CSS -import classes from './CustomQueries.module.css'; - // UI -import { Modal, Icon, Button } from '../../../UI'; +import { Modal, Icon, Button, CompactTable, ActionIcons } from '../../../UI'; // Components import { QueriesForm } from './QueriesForm'; @@ -67,33 +64,27 @@ export const CustomQueries = (): JSX.Element => { )} -
-
- {customQueries.length > 0 && ( - - Name - Prefix - Actions - -
-
- )} - - {customQueries.map((q: Query, idx) => ( - - {q.name} - {q.prefix} - - updateHandler(q)}> - - - deleteHandler(q)}> - - - - - ))} -
+
+ {customQueries.length ? ( + + {customQueries.map((q: Query, idx) => ( + + {q.name} + {q.prefix} + + updateHandler(q)}> + + + deleteHandler(q)}> + + + + + ))} + + ) : ( + <> + )} -
+ ); }; diff --git a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx index 4173c72..61bc6c2 100644 --- a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx +++ b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx @@ -164,10 +164,10 @@ export const GeneralSettings = (): JSX.Element => { - {/* SEARCH SETTINGS */} + {/* === SEARCH OPTIONS === */} - + + {formData.defaultSearchProvider === 'l' && ( + + + + + Will be used when "Local search" is primary search provider and + there are not any local results + + + )} +