diff --git a/.dockerignore b/.dockerignore index c77a412f..5fcee18d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ node_modules -github \ No newline at end of file +github +public \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff227fe9..29550455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -node_modules/ -data/ \ No newline at end of file +node_modules +data +public \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cd99f47e..95ddea40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM node:14-alpine +RUN apk update && apk add --no-cache nano + WORKDIR /app COPY package*.json ./ @@ -10,6 +12,7 @@ COPY . . RUN mkdir -p ./public ./data \ && cd ./client \ + && npm install --production \ && npm run build \ && cd .. \ && mv ./client/build/* ./public \ diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index 0cb17885..808b8157 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -1,5 +1,7 @@ FROM node:14-alpine +RUN apk update && apk add --no-cache nano + WORKDIR /app COPY package*.json ./ @@ -11,6 +13,7 @@ COPY . . RUN mkdir -p ./public ./data \ && cd ./client \ + && npm install --production \ && npm run build \ && cd .. \ && mv ./client/build/* ./public \ diff --git a/README.md b/README.md index 6f4927c4..cca0f49a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,23 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ ![Homescreen screenshot](./github/_themes.png) ## Usage +### Search bar +> While opening links, module will follow `Open all links in the same tab` setting +#### Supported search engines +| Name | Prefix | Search URL | +|------------|--------|-------------------------------------| +| Disroot | /ds | http://search.disroot.org/search?q= | +| DuckDuckGo | /d | https://duckduckgo.com/?q= | +| Google | /g | https://www.google.com/search?q= | + +#### Supported services +| Name | Prefix | Search URL | +|--------------------|--------|-----------------------------------------------| +| IMDb | /im | https://www.imdb.com/find?q= | +| Reddit | /r | -https://www.reddit.com/search?q= | +| The Movie Database | /mv | https://www.themoviedb.org/search?query= | +| Youtube | /yt | https://www.youtube.com/results?search_query= | + ### Setting up weather module 1. Obtain API Key from [Weather API](https://www.weatherapi.com/pricing.aspx). > Free plan allows for 1M calls per month. Flame is making less then 3K API calls per month. @@ -99,6 +116,11 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port` - Redirect: `http://{dest}` +### Custom CSS +> This is an experimental feature. Its behaviour might change in the future. +> +Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) + ## Support If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link: diff --git a/Socket.js b/Socket.js index 0ae1f563..a6a58c64 100644 --- a/Socket.js +++ b/Socket.js @@ -1,11 +1,13 @@ const WebSocket = require('ws'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); class Socket { constructor(server) { this.webSocketServer = new WebSocket.Server({ server }) this.webSocketServer.on('listening', () => { - console.log('Socket: listen'); + logger.log('Socket: listen'); }) this.webSocketServer.on('connection', (webSocketClient) => { diff --git a/api.js b/api.js index 6ea1ab17..a720fe24 100644 --- a/api.js +++ b/api.js @@ -1,15 +1,17 @@ -const path = require('path'); +const { join } = require('path'); const express = require('express'); const errorHandler = require('./middleware/errorHandler'); const api = express(); // Static files -api.use(express.static(path.join(__dirname, 'public'))); +api.use(express.static(join(__dirname, 'public'))); +api.use('/uploads', express.static(join(__dirname, 'data/uploads'))); api.get(/^\/(?!api)/, (req, res) => { - res.sendFile(path.join(__dirname, 'public/index.html')); + res.sendFile(join(__dirname, 'public/index.html')); }) + // Body parser api.use(express.json()); diff --git a/client/.env b/client/.env index 5a3822fd..70acd433 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.0 \ No newline at end of file +REACT_APP_VERSION=1.5.0 \ No newline at end of file diff --git a/client/public/flame.css b/client/public/flame.css new file mode 100644 index 00000000..e69de29b diff --git a/client/public/index.html b/client/public/index.html index 2ede77fe..3f43c404 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -4,15 +4,10 @@ - - - - + + Flame @@ -21,4 +16,4 @@
- + \ No newline at end of file diff --git a/client/public/robots.txt b/client/public/robots.txt index e9e57dc4..77470cb3 100644 --- a/client/public/robots.txt +++ b/client/public/robots.txt @@ -1,3 +1,2 @@ -# https://www.robotstxt.org/robotstxt.html User-agent: * -Disallow: +Disallow: / \ No newline at end of file diff --git a/client/src/components/Apps/AppCard/AppCard.module.css b/client/src/components/Apps/AppCard/AppCard.module.css index 26a8a691..768ef8e6 100644 --- a/client/src/components/Apps/AppCard/AppCard.module.css +++ b/client/src/components/Apps/AppCard/AppCard.module.css @@ -39,4 +39,12 @@ .AppCard:hover { background-color: rgba(0,0,0,0.2); } +} + +.CustomIcon { + width: 90%; + height: 90%; + margin-top: 2px; + margin-left: 2px; + object-fit: contain; } \ No newline at end of file diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 50fb9799..79e09fe0 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -3,6 +3,7 @@ import Icon from '../../UI/Icons/Icon/Icon'; import { iconParser, urlParser } from '../../../utility'; import { App } from '../../../interfaces'; +import { searchConfig } from '../../../utility'; interface ComponentProps { app: App; @@ -15,12 +16,19 @@ const AppCard = (props: ComponentProps): JSX.Element => { return (
- + {(/.(jpeg|jpg|png)$/).test(props.app.icon) + ? {`${props.app.name} + : + }
{props.app.name}
diff --git a/client/src/components/Apps/AppForm/AppForm.module.css b/client/src/components/Apps/AppForm/AppForm.module.css new file mode 100644 index 00000000..66b15a06 --- /dev/null +++ b/client/src/components/Apps/AppForm/AppForm.module.css @@ -0,0 +1,7 @@ +.Switch { + text-decoration: underline; +} + +.Switch:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index e9c7beb4..a8881c2f 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -3,18 +3,23 @@ import { connect } from 'react-redux'; import { addApp, updateApp } from '../../../store/actions'; import { App, NewApp } from '../../../interfaces'; +import classes from './AppForm.module.css'; + import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; +import axios from 'axios'; interface ComponentProps { modalHandler: () => void; - addApp: (formData: NewApp) => any; + addApp: (formData: NewApp | FormData) => any; updateApp: (id: number, formData: NewApp) => any; app?: App; } const AppForm = (props: ComponentProps): JSX.Element => { + const [useCustomIcon, toggleUseCustomIcon] = useState(false); + const [customIcon, setCustomIcon] = useState(null); const [formData, setFormData] = useState({ name: '', url: '', @@ -52,11 +57,27 @@ const AppForm = (props: ComponentProps): JSX.Element => { }) } + const fileChangeHandler = (e: ChangeEvent): void => { + if (e.target.files) { + setCustomIcon(e.target.files[0]); + } + } + const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); if (!props.app) { - props.addApp(formData); + if (customIcon) { + const data = new FormData(); + data.append('icon', customIcon); + + data.append('name', formData.name); + data.append('url', formData.url); + + props.addApp(data); + } else { + props.addApp(formData); + } } else { props.updateApp(props.app.id, formData); props.modalHandler(); @@ -108,26 +129,51 @@ const AppForm = (props: ComponentProps): JSX.Element => {
- - - inputChangeHandler(e)} - /> - - Use icon name from MDI. - - {' '}Click here for reference - - - + {!useCustomIcon + // use mdi icon + ? ( + + inputChangeHandler(e)} + /> + + Use icon name from MDI. + + {' '}Click here for reference + + + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to custom icon upload + + ) + // upload custom icon + : ( + + fileChangeHandler(e)} + accept='.jpg,.jpeg,.png' + /> + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to MDI + + ) + } {!props.app ? : diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 9e8dff92..b0536d92 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -2,7 +2,7 @@ import { Bookmark, Category } from '../../../interfaces'; import classes from './BookmarkCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser, urlParser } from '../../../utility'; +import { iconParser, urlParser, searchConfig } from '../../../utility'; interface ComponentProps { category: Category; @@ -19,7 +19,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { return ( {bookmark.icon && ( diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 078c3a30..2682d505 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; // Redux @@ -22,6 +22,7 @@ import classes from './Home.module.css'; import AppGrid from '../Apps/AppGrid/AppGrid'; import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; +import SearchBox from '../SearchBox/SearchBox'; // Functions import { greeter } from './functions/greeter'; @@ -87,6 +88,11 @@ const Home = (props: ComponentProps): JSX.Element => { return ( + {searchConfig('hideSearch', 0) !== 1 + ? + :
+ } + {searchConfig('hideHeader', 0) !== 1 ? (
@@ -101,24 +107,33 @@ const Home = (props: ComponentProps): JSX.Element => { :
} - - {appsLoading - ? - : app.isPinned)} - totalApps={apps.length} - /> + {searchConfig('hideApps', 0) !== 1 + ? ( + + {appsLoading + ? + : app.isPinned)} + totalApps={apps.length} + /> + } +
+
) + :
} -
- - - {categoriesLoading - ? - : category.isPinned)} - totalCategories={categories.length} - /> + {searchConfig('hideCategories', 0) !== 1 + ? ( + + {categoriesLoading + ? + : category.isPinned)} + totalCategories={categories.length} + /> + } + ) + :
} diff --git a/client/src/components/SearchBox/SearchBox.module.css b/client/src/components/SearchBox/SearchBox.module.css new file mode 100644 index 00000000..d9fbb4e1 --- /dev/null +++ b/client/src/components/SearchBox/SearchBox.module.css @@ -0,0 +1,17 @@ +.SearchBox { + width: 100%; + padding: 10px 0; + color: var(--color-primary); + /* font-size: 20px; */ + margin-bottom: 20px; + background-color: transparent; + border: none; + border-bottom: 2px solid var(--color-accent); + opacity: 0.5; + transition: all 0.2s; +} + +.SearchBox:focus { + opacity: 1; + outline: none; +} \ No newline at end of file diff --git a/client/src/components/SearchBox/SearchBox.tsx b/client/src/components/SearchBox/SearchBox.tsx new file mode 100644 index 00000000..ebc0cc6c --- /dev/null +++ b/client/src/components/SearchBox/SearchBox.tsx @@ -0,0 +1,29 @@ +import { useRef, useEffect, KeyboardEvent } from 'react'; + +import classes from './SearchBox.module.css'; +import { searchParser } from '../../utility'; + +const SearchBox = (): JSX.Element => { + const inputRef = useRef(document.createElement('input')); + + useEffect(() => { + inputRef.current.focus(); + }, []) + + const searchHandler = (e: KeyboardEvent) => { + if (e.code === 'Enter') { + searchParser(inputRef.current.value); + } + } + + return ( + searchHandler(e)} + /> + ) +} + +export default SearchBox; \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.module.css b/client/src/components/Settings/OtherSettings/OtherSettings.module.css new file mode 100644 index 00000000..36e4deb0 --- /dev/null +++ b/client/src/components/Settings/OtherSettings/OtherSettings.module.css @@ -0,0 +1,9 @@ +.SettingsSection { + color: var(--color-primary); + padding-bottom: 3px; + margin-bottom: 10px; + font-size: 20px; + font-weight: 500; + border-bottom: 2px solid var(--color-accent); + display: inline-block; +} \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index bba197d1..329b068f 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -11,6 +11,9 @@ import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces' import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; +// CSS +import classes from './OtherSettings.module.css'; + // Utils import { searchConfig } from '../../../utility'; @@ -29,7 +32,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: 1, pinCategoriesByDefault: 1, hideHeader: 0, - useOrdering: 'createdAt' + hideApps: 0, + hideCategories: 0, + hideSearch: 0, + useOrdering: 'createdAt', + openSameTab: 0 }) // Get config @@ -39,7 +46,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: searchConfig('pinAppsByDefault', 1), pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1), hideHeader: searchConfig('hideHeader', 0), - useOrdering: searchConfig('useOrdering', 'createdAt') + hideApps: searchConfig('hideApps', 0), + hideCategories: searchConfig('hideCategories', 0), + hideSearch: searchConfig('hideSearch', 0), + useOrdering: searchConfig('useOrdering', 'createdAt'), + openSameTab: searchConfig('openSameTab', 0) }) }, [props.loading]); @@ -74,6 +85,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { return (
formSubmitHandler(e)}> + {/* OTHER OPTIONS */} +

Miscellaneous

{ onChange={(e) => inputChangeHandler(e)} /> + + {/* BEAHVIOR OPTIONS */} +

App Behavior

+ + + + + + + + + + {/* MODULES OPTIONS */} +

Modules

+ + + + - + + + + + diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index 49b08bd3..b1eb300d 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -9,6 +9,7 @@ import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; import AppDetails from './AppDetails/AppDetails'; +import StyleSettings from './StyleSettings/StyleSettings'; const Settings = (): JSX.Element => { return ( @@ -40,6 +41,13 @@ const Settings = (): JSX.Element => { to='/settings/other'> Other + + CSS + { + diff --git a/client/src/components/Settings/StyleSettings/StyleSettings.tsx b/client/src/components/Settings/StyleSettings/StyleSettings.tsx new file mode 100644 index 00000000..9f450658 --- /dev/null +++ b/client/src/components/Settings/StyleSettings/StyleSettings.tsx @@ -0,0 +1,63 @@ +import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; +import axios from 'axios'; + +// Redux +import { connect } from 'react-redux'; +import { createNotification } from '../../../store/actions'; + +// Typescript +import { ApiResponse, NewNotification } from '../../../interfaces'; + +// UI +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; +import Button from '../../UI/Buttons/Button/Button'; + +interface ComponentProps { + createNotification: (notification: NewNotification) => void; +} + +const StyleSettings = (props: ComponentProps): JSX.Element => { + const [customStyles, setCustomStyles] = useState(''); + + useEffect(() => { + axios.get>('/api/config/0/css') + .then(data => setCustomStyles(data.data.data)) + .catch(err => console.log(err.response)); + }, []) + + const inputChangeHandler = (e: ChangeEvent) => { + e.preventDefault(); + setCustomStyles(e.target.value); + } + + const formSubmitHandler = (e: FormEvent) => { + e.preventDefault(); + + axios.put>('/api/config/0/css', { styles: customStyles }) + .then(() => { + props.createNotification({ + title: 'Success', + message: 'CSS saved. Reload page to see changes' + }) + }) + .catch(err => console.log(err.response)); + } + + return ( + formSubmitHandler(e)}> + + + + + + + ) +} + +export default connect(null, { createNotification })(StyleSettings); \ No newline at end of file diff --git a/client/src/components/UI/Forms/InputGroup/InputGroup.module.css b/client/src/components/UI/Forms/InputGroup/InputGroup.module.css index 62417640..93b74f15 100644 --- a/client/src/components/UI/Forms/InputGroup/InputGroup.module.css +++ b/client/src/components/UI/Forms/InputGroup/InputGroup.module.css @@ -4,12 +4,14 @@ .InputGroup label, .InputGroup span, -.InputGroup input { +.InputGroup input, +.InputGroup textarea { display: block; } .InputGroup input, -.InputGroup select { +.InputGroup select, +.InputGroup textarea { margin: 8px 0; width: 100%; border: none; @@ -30,4 +32,9 @@ .InputGroup label { color: var(--color-primary); +} + +.InputGroup textarea { + resize: none; + height: 50vh; } \ No newline at end of file diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 360dae15..b5e58eee 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -10,5 +10,9 @@ export interface SettingsForm { pinAppsByDefault: number; pinCategoriesByDefault: number; hideHeader: number; + hideApps: number; + hideCategories: number; + hideSearch: number; useOrdering: string; + openSameTab: number; } \ No newline at end of file diff --git a/client/src/interfaces/Query.ts b/client/src/interfaces/Query.ts new file mode 100644 index 00000000..b636bc43 --- /dev/null +++ b/client/src/interfaces/Query.ts @@ -0,0 +1,5 @@ +export interface Query { + name: string; + prefix: string; + template: string; +} \ No newline at end of file diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 2f333d31..6f751ba2 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -7,4 +7,5 @@ export * from './Bookmark'; export * from './Category'; export * from './Notification'; export * from './Config'; -export * from './Forms'; \ No newline at end of file +export * from './Forms'; +export * from './Query'; \ No newline at end of file diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js index 5cafcb16..b45d4fee 100644 --- a/client/src/setupProxy.js +++ b/client/src/setupProxy.js @@ -5,11 +5,16 @@ module.exports = function (app) { target: 'http://localhost:5005' }) + const assetsProxy = createProxyMiddleware('/uploads', { + target: 'http://localhost:5005' + }) + const wsProxy = createProxyMiddleware('/socket', { target: 'http://localhost:5005', ws: true }) app.use(apiProxy); + app.use(assetsProxy); app.use(wsProxy); }; \ No newline at end of file diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 97db1c70..3a8e7d54 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -61,7 +61,7 @@ export interface AddAppAction { payload: App; } -export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { +export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.post>('/api/apps', formData); @@ -69,7 +69,7 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { type: ActionTypes.createNotification, payload: { title: 'Success', - message: `App ${formData.name} added` + message: `App added` } }) @@ -116,7 +116,7 @@ export interface UpdateAppAction { payload: App; } -export const updateApp = (id: number, formData: NewApp) => async (dispatch: Dispatch) => { +export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.put>(`/api/apps/${id}`, formData); @@ -124,7 +124,7 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp type: ActionTypes.createNotification, payload: { title: 'Success', - message: `App ${formData.name} updated` + message: `App updated` } }) diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index a5407b20..99f8d698 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -2,4 +2,5 @@ export * from './iconParser'; export * from './urlParser'; export * from './searchConfig'; export * from './checkVersion'; -export * from './sortData'; \ No newline at end of file +export * from './sortData'; +export * from './searchParser'; \ No newline at end of file diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts new file mode 100644 index 00000000..4bea43f4 --- /dev/null +++ b/client/src/utility/searchParser.ts @@ -0,0 +1,22 @@ +import { queries } from './searchQueries.json'; +import { Query } from '../interfaces'; + +import { searchConfig } from '.'; + +export const searchParser = (searchQuery: string): void => { + const space = searchQuery.indexOf(' '); + const prefix = searchQuery.slice(1, space); + const search = encodeURIComponent(searchQuery.slice(space + 1)); + + const query = queries.find((q: Query) => q.prefix === prefix); + + if (query) { + const sameTab = searchConfig('openSameTab', false); + + if (sameTab) { + document.location.replace(`${query.template}${search}`); + } else { + window.open(`${query.template}${search}`); + } + } +} \ No newline at end of file diff --git a/client/src/utility/searchQueries.json b/client/src/utility/searchQueries.json new file mode 100644 index 00000000..47739c96 --- /dev/null +++ b/client/src/utility/searchQueries.json @@ -0,0 +1,39 @@ +{ + "queries": [ + { + "name": "Google", + "prefix": "g", + "template": "https://www.google.com/search?q=" + }, + { + "name": "DuckDuckGo", + "prefix": "d", + "template": "https://duckduckgo.com/?q=" + }, + { + "name": "Disroot", + "prefix": "ds", + "template": "http://search.disroot.org/search?q=" + }, + { + "name": "YouTube", + "prefix": "yt", + "template": "https://www.youtube.com/results?search_query=" + }, + { + "name": "Reddit", + "prefix": "r", + "template": "https://www.reddit.com/search?q=" + }, + { + "name": "IMDb", + "prefix": "im", + "template": "https://www.imdb.com/find?q=" + }, + { + "name": "The Movie Database", + "prefix": "mv", + "template": "https://www.themoviedb.org/search?query=" + } + ] +} \ No newline at end of file diff --git a/controllers/apps.js b/controllers/apps.js index 4f50f96a..238c66b3 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -14,11 +14,17 @@ exports.createApp = asyncWrapper(async (req, res, next) => { }); let app; + let _body = { ...req.body }; + + if (req.file) { + _body.icon = req.file.filename; + } + if (pinApps) { if (parseInt(pinApps.value)) { app = await App.create({ - ...req.body, + ..._body, isPinned: true }) } else { diff --git a/controllers/config.js b/controllers/config.js index f8f3613c..ef4f7073 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -2,6 +2,8 @@ const asyncWrapper = require('../middleware/asyncWrapper'); const ErrorResponse = require('../utils/ErrorResponse'); const Config = require('../models/Config'); const { Op } = require('sequelize'); +const File = require('../utils/File'); +const { join } = require('path'); // @desc Insert new key:value pair // @route POST /api/config @@ -122,6 +124,33 @@ exports.deletePair = asyncWrapper(async (req, res, next) => { await pair.destroy(); + res.status(200).json({ + success: true, + data: {} + }) +}) + +// @desc Get custom CSS file +// @route GET /api/config/0/css +// @access Public +exports.getCss = asyncWrapper(async (req, res, next) => { + const file = new File(join(__dirname, '../public/flame.css')); + const content = file.read(); + + res.status(200).json({ + success: true, + data: content + }) +}) + + +// @desc Update custom CSS file +// @route PUT /api/config/0/css +// @access Public +exports.updateCss = asyncWrapper(async (req, res, next) => { + const file = new File(join(__dirname, '../public/flame.css')); + file.write(req.body.styles); + res.status(200).json({ success: true, data: {} diff --git a/db.js b/db.js index bc4c536c..9761efe9 100644 --- a/db.js +++ b/db.js @@ -1,24 +1,32 @@ const { Sequelize } = require('sequelize'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); const sequelize = new Sequelize({ dialect: 'sqlite', storage: './data/db.sqlite', logging: false -}); +}) const connectDB = async () => { try { await sequelize.authenticate(); - console.log('Connected to database'); + logger.log('Connected to database'); - await sequelize.sync({ alter: true }); - console.log('All models were synced'); + const syncModels = true; + + if (syncModels) { + logger.log('Starting model synchronization'); + await sequelize.sync({ alter: true }); + logger.log('All models were synchronized'); + } } catch (error) { - console.error('Unable to connect to the database:', error); + logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR'); + process.exit(1); } } module.exports = { connectDB, sequelize -}; \ No newline at end of file +} \ No newline at end of file diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js index 2de45a7b..5db2bb24 100644 --- a/middleware/errorHandler.js +++ b/middleware/errorHandler.js @@ -1,5 +1,7 @@ const ErrorResponse = require('../utils/ErrorResponse'); const colors = require('colors'); +const Logger = require('../utils/Logger'); +const logger = new Logger(); const errorHandler = (err, req, res, next) => { let error = { ...err }; @@ -10,8 +12,7 @@ const errorHandler = (err, req, res, next) => { // error = new ErrorResponse(`Field ${msg}`, 400); // } - console.log(error); - console.log(`${err}`); + logger.log(error.message.split(',')[0], 'ERROR'); res.status(err.statusCode || 500).json({ success: false, diff --git a/middleware/multer.js b/middleware/multer.js new file mode 100644 index 00000000..8119477d --- /dev/null +++ b/middleware/multer.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const multer = require('multer'); + +if (!fs.existsSync('data/uploads')) { + fs.mkdirSync('data/uploads'); +} + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, './data/uploads'); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + '--' + file.originalname); + } +}) + +const supportedTypes = ['jpg', 'jpeg', 'png']; + +const fileFilter = (req, file, cb) => { + if (supportedTypes.includes(file.mimetype.split('/')[1])) { + cb(null, true); + } else { + cb(null, false); + } +} + +const upload = multer({ storage, fileFilter }); + +module.exports = upload.single('icon'); \ No newline at end of file diff --git a/models/associateModels.js b/models/associateModels.js index 24570921..d1b86c18 100644 --- a/models/associateModels.js +++ b/models/associateModels.js @@ -2,12 +2,14 @@ const Category = require('./Category'); const Bookmark = require('./Bookmark'); const associateModels = () => { - // Category <> Bookmark Category.hasMany(Bookmark, { - as: 'bookmarks', + foreignKey: 'categoryId', + as: 'bookmarks' + }); + + Bookmark.belongsTo(Category, { foreignKey: 'categoryId' }); - Bookmark.belongsTo(Category, { foreignKey: 'categoryId' }); } module.exports = associateModels; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6ac480ad..21017a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,6 +224,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -364,6 +369,43 @@ "fill-range": "^7.0.1" } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -553,6 +595,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "concurrently": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.2.tgz", @@ -741,6 +794,38 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -1611,6 +1696,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "needle": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", @@ -2411,6 +2511,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -2577,6 +2682,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -2804,6 +2914,11 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b958be88..3150454c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "concurrently": "^6.0.2", "dotenv": "^9.0.0", "express": "^4.17.1", + "multer": "^1.4.2", "node-schedule": "^2.0.0", "sequelize": "^6.6.2", "sqlite3": "^5.0.2", diff --git a/routes/apps.js b/routes/apps.js index a0b3f47d..091550c1 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const upload = require('../middleware/multer'); const { createApp, @@ -12,7 +13,7 @@ const { router .route('/') - .post(createApp) + .post(upload, createApp) .get(getApps); router diff --git a/routes/config.js b/routes/config.js index caadbe58..eebf5dd7 100644 --- a/routes/config.js +++ b/routes/config.js @@ -8,6 +8,8 @@ const { updateValue, updateValues, deletePair, + updateCss, + getCss, } = require('../controllers/config'); router @@ -22,4 +24,9 @@ router .put(updateValue) .delete(deletePair); +router + .route('/0/css') + .get(getCss) + .put(updateCss); + module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index 569b7ac2..6ce25d07 100644 --- a/server.js +++ b/server.js @@ -7,23 +7,25 @@ const Socket = require('./Socket'); const Sockets = require('./Sockets'); const associateModels = require('./models/associateModels'); const initConfig = require('./utils/initConfig'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); const PORT = process.env.PORT || 5005; -connectDB() - .then(() => { - associateModels(); - initConfig(); - }); +(async () => { + await connectDB(); + await associateModels(); + await initConfig(); -// Create server for Express API and WebSockets -const server = http.createServer(); -server.on('request', api); + // Create server for Express API and WebSockets + const server = http.createServer(); + server.on('request', api); -// Register weatherSocket -const weatherSocket = new Socket(server); -Sockets.registerSocket('weather', weatherSocket); + // Register weatherSocket + const weatherSocket = new Socket(server); + Sockets.registerSocket('weather', weatherSocket); -server.listen(PORT, () => { - console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); -}) \ No newline at end of file + server.listen(PORT, () => { + logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); + }) +})(); \ No newline at end of file diff --git a/utils/File.js b/utils/File.js new file mode 100644 index 00000000..0b2fbdc3 --- /dev/null +++ b/utils/File.js @@ -0,0 +1,25 @@ +const fs = require('fs'); + +class File { + constructor(path) { + this.path = path; + this.content = ''; + } + + read() { + try { + const content = fs.readFileSync(this.path, { encoding: 'utf-8' }); + this.content = content; + return this.content; + } catch (err) { + return err.message; + } + } + + write(data) { + this.content = data; + fs.writeFileSync(this.path, this.content); + } +} + +module.exports = File; \ No newline at end of file diff --git a/utils/Logger.js b/utils/Logger.js index 6301dfe8..1d1deef1 100644 --- a/utils/Logger.js +++ b/utils/Logger.js @@ -1,40 +1,39 @@ -const fs = require('fs'); - class Logger { - constructor() { - this.logFileHandler(); + log(message, level = 'INFO') { + console.log(`[${this.generateTimestamp()}] [${level}] ${message}`) } - logFileHandler() { - if (!fs.existsSync('./flame.log')) { - fs.writeFileSync('./flame.log', ''); - } else { - console.log('file exists'); - } - } + generateTimestamp() { + const d = new Date(); - writeLog(logMsg, logType) { + // Date + const year = d.getFullYear(); + const month = this.parseDate(d.getMonth() + 1); + const day = this.parseDate(d.getDate()); - } + // Time + const hour = this.parseDate(d.getHours()); + const minutes = this.parseDate(d.getMinutes()); + const seconds = this.parseDate(d.getSeconds()); + const miliseconds = this.parseDate(d.getMilliseconds(), true); - generateLog(logMsg, logType) { - const now = new Date(); - const date = `${this.parseNumber(now.getDate())}-${this.parseNumber(now.getMonth() + 1)}-${now.getFullYear()}`; - const time = `${this.parseNumber(now.getHours())}:${this.parseNumber(now.getMinutes())}:${this.parseNumber(now.getSeconds())}.${now.getMilliseconds()}`; - const log = `[${date} ${time}]: ${logType} ${logMsg}`; - return log; - // const timestamp = new Date().toISOString(); + // Timezone + const tz = -d.getTimezoneOffset() / 60; + + return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${tz >= 0 ? '+' + tz : tz}`; } - parseNumber(number) { - if (number > 9) { - return number; - } else { - return `0${number}`; + parseDate(date, ms = false) { + if (ms) { + if (date >= 10 && date < 100) { + return `0${date}`; + } else if (date < 10) { + return `00${date}`; + } } + + return date < 10 ? `0${date}` : date.toString(); } } -// console.log(logger.generateLog('testMsg', 'INFO')); - -module.exports = new Logger(); \ No newline at end of file +module.exports = Logger; \ No newline at end of file diff --git a/utils/clearWeatherData.js b/utils/clearWeatherData.js index d43f975a..07be15bb 100644 --- a/utils/clearWeatherData.js +++ b/utils/clearWeatherData.js @@ -1,5 +1,7 @@ const { Op } = require('sequelize'); const Weather = require('../models/Weather'); +const Logger = require('./Logger'); +const logger = new Logger(); const clearWeatherData = async () => { const weather = await Weather.findOne({ @@ -16,7 +18,7 @@ const clearWeatherData = async () => { }) } - console.log('Old weather data was deleted'); + logger.log('Old weather data was deleted'); } module.exports = clearWeatherData; \ No newline at end of file diff --git a/utils/initConfig.js b/utils/initConfig.js index de1cc881..1455ccbd 100644 --- a/utils/initConfig.js +++ b/utils/initConfig.js @@ -1,6 +1,8 @@ const { Op } = require('sequelize'); const Config = require('../models/Config'); const { config } = require('./initialConfig.json'); +const Logger = require('./Logger'); +const logger = new Logger(); const initConfig = async () => { // Get config values @@ -26,7 +28,7 @@ const initConfig = async () => { } }) - console.log('Initial config created'); + logger.log('Initial config created'); return; } diff --git a/utils/initialConfig.json b/utils/initialConfig.json index 09bf4b8e..fe68dfe2 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -35,6 +35,22 @@ { "key": "useOrdering", "value": "createdAt" + }, + { + "key": "openSameTab", + "value": false + }, + { + "key": "hideApps", + "value": false + }, + { + "key": "hideCategories", + "value": false + }, + { + "key": "hideSearch", + "value": false } ] } \ No newline at end of file diff --git a/utils/jobs.js b/utils/jobs.js index 19dc0a82..935f4979 100644 --- a/utils/jobs.js +++ b/utils/jobs.js @@ -2,15 +2,17 @@ const schedule = require('node-schedule'); const getExternalWeather = require('./getExternalWeather'); const clearWeatherData = require('./clearWeatherData'); const Sockets = require('../Sockets'); +const Logger = require('./Logger'); +const logger = new Logger(); // Update weather data every 15 minutes const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => { try { const weatherData = await getExternalWeather(); - console.log('weather updated'); + logger.log('Weather updated'); Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData)); } catch (err) { - console.log(err.message); + logger.log(err.message, 'ERROR'); } })