commit
aec00982ba
49 changed files with 778 additions and 142 deletions
|
@ -1,2 +1,3 @@
|
||||||
node_modules
|
node_modules
|
||||||
github
|
github
|
||||||
|
public
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
node_modules/
|
node_modules
|
||||||
data/
|
data
|
||||||
|
public
|
|
@ -1,5 +1,7 @@
|
||||||
FROM node:14-alpine
|
FROM node:14-alpine
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache nano
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
FROM node:14-alpine
|
FROM node:14-alpine
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache nano
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
@ -11,6 +13,7 @@ COPY . .
|
||||||
|
|
||||||
RUN mkdir -p ./public ./data \
|
RUN mkdir -p ./public ./data \
|
||||||
&& cd ./client \
|
&& cd ./client \
|
||||||
|
&& npm install --production \
|
||||||
&& npm run build \
|
&& npm run build \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
&& mv ./client/build/* ./public \
|
&& mv ./client/build/* ./public \
|
||||||
|
|
22
README.md
22
README.md
|
@ -81,6 +81,23 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/
|
||||||
![Homescreen screenshot](./github/_themes.png)
|
![Homescreen screenshot](./github/_themes.png)
|
||||||
|
|
||||||
## Usage
|
## 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
|
### Setting up weather module
|
||||||
1. Obtain API Key from [Weather API](https://www.weatherapi.com/pricing.aspx).
|
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.
|
> 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`
|
- Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port`
|
||||||
- Redirect: `http://{dest}`
|
- 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
|
## Support
|
||||||
If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link:
|
If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link:
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
const Logger = require('./utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
class Socket {
|
class Socket {
|
||||||
constructor(server) {
|
constructor(server) {
|
||||||
this.webSocketServer = new WebSocket.Server({ server })
|
this.webSocketServer = new WebSocket.Server({ server })
|
||||||
|
|
||||||
this.webSocketServer.on('listening', () => {
|
this.webSocketServer.on('listening', () => {
|
||||||
console.log('Socket: listen');
|
logger.log('Socket: listen');
|
||||||
})
|
})
|
||||||
|
|
||||||
this.webSocketServer.on('connection', (webSocketClient) => {
|
this.webSocketServer.on('connection', (webSocketClient) => {
|
||||||
|
|
8
api.js
8
api.js
|
@ -1,15 +1,17 @@
|
||||||
const path = require('path');
|
const { join } = require('path');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const errorHandler = require('./middleware/errorHandler');
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
|
|
||||||
const api = express();
|
const api = express();
|
||||||
|
|
||||||
// Static files
|
// 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) => {
|
api.get(/^\/(?!api)/, (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public/index.html'));
|
res.sendFile(join(__dirname, 'public/index.html'));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// Body parser
|
// Body parser
|
||||||
api.use(express.json());
|
api.use(express.json());
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
REACT_APP_VERSION=1.4.0
|
REACT_APP_VERSION=1.5.0
|
0
client/public/flame.css
Normal file
0
client/public/flame.css
Normal file
|
@ -4,15 +4,10 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="description" content="Flame - self-hosted startpage for your server" />
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="%PUBLIC_URL%/flame.css">
|
||||||
<title>Flame</title>
|
<title>Flame</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -21,4 +16,4 @@
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,3 +1,2 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow: /
|
|
@ -39,4 +39,12 @@
|
||||||
.AppCard:hover {
|
.AppCard:hover {
|
||||||
background-color: rgba(0,0,0,0.2);
|
background-color: rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CustomIcon {
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
import { iconParser, urlParser } from '../../../utility';
|
import { iconParser, urlParser } from '../../../utility';
|
||||||
|
|
||||||
import { App } from '../../../interfaces';
|
import { App } from '../../../interfaces';
|
||||||
|
import { searchConfig } from '../../../utility';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
app: App;
|
app: App;
|
||||||
|
@ -15,12 +16,19 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={redirectUrl}
|
href={redirectUrl}
|
||||||
target='_blank'
|
target={searchConfig('openSameTab', false) ? '' : '_blank'}
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
className={classes.AppCard}
|
className={classes.AppCard}
|
||||||
>
|
>
|
||||||
<div className={classes.AppCardIcon}>
|
<div className={classes.AppCardIcon}>
|
||||||
<Icon icon={iconParser(props.app.icon)} />
|
{(/.(jpeg|jpg|png)$/).test(props.app.icon)
|
||||||
|
? <img
|
||||||
|
src={`/uploads/${props.app.icon}`}
|
||||||
|
alt={`${props.app.name} icon`}
|
||||||
|
className={classes.CustomIcon}
|
||||||
|
/>
|
||||||
|
: <Icon icon={iconParser(props.app.icon)} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.AppCardDetails}>
|
<div className={classes.AppCardDetails}>
|
||||||
<h5>{props.app.name}</h5>
|
<h5>{props.app.name}</h5>
|
||||||
|
|
7
client/src/components/Apps/AppForm/AppForm.module.css
Normal file
7
client/src/components/Apps/AppForm/AppForm.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.Switch {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Switch:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -3,18 +3,23 @@ import { connect } from 'react-redux';
|
||||||
import { addApp, updateApp } from '../../../store/actions';
|
import { addApp, updateApp } from '../../../store/actions';
|
||||||
import { App, NewApp } from '../../../interfaces';
|
import { App, NewApp } from '../../../interfaces';
|
||||||
|
|
||||||
|
import classes from './AppForm.module.css';
|
||||||
|
|
||||||
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
|
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
|
||||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||||
import Button from '../../UI/Buttons/Button/Button';
|
import Button from '../../UI/Buttons/Button/Button';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
modalHandler: () => void;
|
modalHandler: () => void;
|
||||||
addApp: (formData: NewApp) => any;
|
addApp: (formData: NewApp | FormData) => any;
|
||||||
updateApp: (id: number, formData: NewApp) => any;
|
updateApp: (id: number, formData: NewApp) => any;
|
||||||
app?: App;
|
app?: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppForm = (props: ComponentProps): JSX.Element => {
|
const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
|
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
|
||||||
|
const [customIcon, setCustomIcon] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState<NewApp>({
|
const [formData, setFormData] = useState<NewApp>({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
@ -52,11 +57,27 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
if (e.target.files) {
|
||||||
|
setCustomIcon(e.target.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formSubmitHandler = (e: SyntheticEvent<HTMLFormElement>): void => {
|
const formSubmitHandler = (e: SyntheticEvent<HTMLFormElement>): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!props.app) {
|
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 {
|
} else {
|
||||||
props.updateApp(props.app.id, formData);
|
props.updateApp(props.app.id, formData);
|
||||||
props.modalHandler();
|
props.modalHandler();
|
||||||
|
@ -108,26 +129,51 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
{!useCustomIcon
|
||||||
<label htmlFor='icon'>App Icon</label>
|
// use mdi icon
|
||||||
<input
|
? (<InputGroup>
|
||||||
type='text'
|
<label htmlFor='icon'>App Icon</label>
|
||||||
name='icon'
|
<input
|
||||||
id='icon'
|
type='text'
|
||||||
placeholder='book-open-outline'
|
name='icon'
|
||||||
required
|
id='icon'
|
||||||
value={formData.icon}
|
placeholder='book-open-outline'
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
required
|
||||||
/>
|
value={formData.icon}
|
||||||
<span>
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
Use icon name from MDI.
|
/>
|
||||||
<a
|
<span>
|
||||||
href='https://materialdesignicons.com/'
|
Use icon name from MDI.
|
||||||
target='blank'>
|
<a
|
||||||
{' '}Click here for reference
|
href='https://materialdesignicons.com/'
|
||||||
</a>
|
target='blank'>
|
||||||
</span>
|
{' '}Click here for reference
|
||||||
</InputGroup>
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
|
||||||
|
className={classes.Switch}>
|
||||||
|
Switch to custom icon upload
|
||||||
|
</span>
|
||||||
|
</InputGroup>)
|
||||||
|
// upload custom icon
|
||||||
|
: (<InputGroup>
|
||||||
|
<label htmlFor='icon'>App Icon</label>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
name='icon'
|
||||||
|
id='icon'
|
||||||
|
required
|
||||||
|
onChange={(e) => fileChangeHandler(e)}
|
||||||
|
accept='.jpg,.jpeg,.png'
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
|
||||||
|
className={classes.Switch}>
|
||||||
|
Switch to MDI
|
||||||
|
</span>
|
||||||
|
</InputGroup>)
|
||||||
|
}
|
||||||
{!props.app
|
{!props.app
|
||||||
? <Button>Add new application</Button>
|
? <Button>Add new application</Button>
|
||||||
: <Button>Update application</Button>
|
: <Button>Update application</Button>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Bookmark, Category } from '../../../interfaces';
|
||||||
import classes from './BookmarkCard.module.css';
|
import classes from './BookmarkCard.module.css';
|
||||||
|
|
||||||
import Icon from '../../UI/Icons/Icon/Icon';
|
import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
import { iconParser, urlParser } from '../../../utility';
|
import { iconParser, urlParser, searchConfig } from '../../../utility';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
category: Category;
|
category: Category;
|
||||||
|
@ -19,7 +19,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={redirectUrl}
|
href={redirectUrl}
|
||||||
target='_blank'
|
target={searchConfig('openSameTab', false) ? '' : '_blank'}
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
key={`bookmark-${bookmark.id}`}>
|
key={`bookmark-${bookmark.id}`}>
|
||||||
{bookmark.icon && (
|
{bookmark.icon && (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, Fragment } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
|
@ -22,6 +22,7 @@ import classes from './Home.module.css';
|
||||||
import AppGrid from '../Apps/AppGrid/AppGrid';
|
import AppGrid from '../Apps/AppGrid/AppGrid';
|
||||||
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
|
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
|
||||||
import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
|
import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
|
||||||
|
import SearchBox from '../SearchBox/SearchBox';
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
import { greeter } from './functions/greeter';
|
import { greeter } from './functions/greeter';
|
||||||
|
@ -87,6 +88,11 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
{searchConfig('hideSearch', 0) !== 1
|
||||||
|
? <SearchBox />
|
||||||
|
: <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
{searchConfig('hideHeader', 0) !== 1
|
{searchConfig('hideHeader', 0) !== 1
|
||||||
? (
|
? (
|
||||||
<header className={classes.Header}>
|
<header className={classes.Header}>
|
||||||
|
@ -101,24 +107,33 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
: <div></div>
|
: <div></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<SectionHeadline title='Applications' link='/applications' />
|
{searchConfig('hideApps', 0) !== 1
|
||||||
{appsLoading
|
? (<Fragment>
|
||||||
? <Spinner />
|
<SectionHeadline title='Applications' link='/applications' />
|
||||||
: <AppGrid
|
{appsLoading
|
||||||
apps={apps.filter((app: App) => app.isPinned)}
|
? <Spinner />
|
||||||
totalApps={apps.length}
|
: <AppGrid
|
||||||
/>
|
apps={apps.filter((app: App) => app.isPinned)}
|
||||||
|
totalApps={apps.length}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<div className={classes.HomeSpace}></div>
|
||||||
|
</Fragment>)
|
||||||
|
: <div></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={classes.HomeSpace}></div>
|
{searchConfig('hideCategories', 0) !== 1
|
||||||
|
? (<Fragment>
|
||||||
<SectionHeadline title='Bookmarks' link='/bookmarks' />
|
<SectionHeadline title='Bookmarks' link='/bookmarks' />
|
||||||
{categoriesLoading
|
{categoriesLoading
|
||||||
? <Spinner />
|
? <Spinner />
|
||||||
: <BookmarkGrid
|
: <BookmarkGrid
|
||||||
categories={categories.filter((category: Category) => category.isPinned)}
|
categories={categories.filter((category: Category) => category.isPinned)}
|
||||||
totalCategories={categories.length}
|
totalCategories={categories.length}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
</Fragment>)
|
||||||
|
: <div></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Link to='/settings' className={classes.SettingsButton}>
|
<Link to='/settings' className={classes.SettingsButton}>
|
||||||
|
|
17
client/src/components/SearchBox/SearchBox.module.css
Normal file
17
client/src/components/SearchBox/SearchBox.module.css
Normal file
|
@ -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;
|
||||||
|
}
|
29
client/src/components/SearchBox/SearchBox.tsx
Normal file
29
client/src/components/SearchBox/SearchBox.tsx
Normal file
|
@ -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<HTMLInputElement>(document.createElement('input'));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
searchParser(inputRef.current.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type='text'
|
||||||
|
className={classes.SearchBox}
|
||||||
|
onKeyDown={(e) => searchHandler(e)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBox;
|
|
@ -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;
|
||||||
|
}
|
|
@ -11,6 +11,9 @@ import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces'
|
||||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||||
import Button from '../../UI/Buttons/Button/Button';
|
import Button from '../../UI/Buttons/Button/Button';
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
import classes from './OtherSettings.module.css';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchConfig } from '../../../utility';
|
import { searchConfig } from '../../../utility';
|
||||||
|
|
||||||
|
@ -29,7 +32,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
pinAppsByDefault: 1,
|
pinAppsByDefault: 1,
|
||||||
pinCategoriesByDefault: 1,
|
pinCategoriesByDefault: 1,
|
||||||
hideHeader: 0,
|
hideHeader: 0,
|
||||||
useOrdering: 'createdAt'
|
hideApps: 0,
|
||||||
|
hideCategories: 0,
|
||||||
|
hideSearch: 0,
|
||||||
|
useOrdering: 'createdAt',
|
||||||
|
openSameTab: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
|
@ -39,7 +46,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
|
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
|
||||||
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
|
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
|
||||||
hideHeader: searchConfig('hideHeader', 0),
|
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]);
|
}, [props.loading]);
|
||||||
|
|
||||||
|
@ -74,6 +85,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||||
|
{/* OTHER OPTIONS */}
|
||||||
|
<h2 className={classes.SettingsSection}>Miscellaneous</h2>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='customTitle'>Custom page title</label>
|
<label htmlFor='customTitle'>Custom page title</label>
|
||||||
<input
|
<input
|
||||||
|
@ -85,6 +98,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* BEAHVIOR OPTIONS */}
|
||||||
|
<h2 className={classes.SettingsSection}>App Behavior</h2>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='pinAppsByDefault'>Pin new applications by default</label>
|
<label htmlFor='pinAppsByDefault'>Pin new applications by default</label>
|
||||||
<select
|
<select
|
||||||
|
@ -109,6 +125,46 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor='useOrdering'>Sorting type</label>
|
||||||
|
<select
|
||||||
|
id='useOrdering'
|
||||||
|
name='useOrdering'
|
||||||
|
value={formData.useOrdering}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
>
|
||||||
|
<option value='createdAt'>By creation date</option>
|
||||||
|
<option value='name'>Alphabetical order</option>
|
||||||
|
<option value='orderId'>Custom order</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor='openSameTab'>Open all links in the same tab</label>
|
||||||
|
<select
|
||||||
|
id='openSameTab'
|
||||||
|
name='openSameTab'
|
||||||
|
value={formData.openSameTab}
|
||||||
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* MODULES OPTIONS */}
|
||||||
|
<h2 className={classes.SettingsSection}>Modules</h2>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor='hideSearch'>Hide search bar</label>
|
||||||
|
<select
|
||||||
|
id='hideSearch'
|
||||||
|
name='hideSearch'
|
||||||
|
value={formData.hideSearch}
|
||||||
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='hideHeader'>Hide greeting and date</label>
|
<label htmlFor='hideHeader'>Hide greeting and date</label>
|
||||||
<select
|
<select
|
||||||
|
@ -122,16 +178,27 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='useOrdering'>Sorting type</label>
|
<label htmlFor='hideApps'>Hide applications</label>
|
||||||
<select
|
<select
|
||||||
id='useOrdering'
|
id='hideApps'
|
||||||
name='useOrdering'
|
name='hideApps'
|
||||||
value={formData.useOrdering}
|
value={formData.hideApps}
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
>
|
>
|
||||||
<option value='createdAt'>By creation date</option>
|
<option value={1}>True</option>
|
||||||
<option value='name'>Alphabetical order</option>
|
<option value={0}>False</option>
|
||||||
<option value='orderId'>Custom order</option>
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor='hideCategories'>Hide categories</label>
|
||||||
|
<select
|
||||||
|
id='hideCategories'
|
||||||
|
name='hideCategories'
|
||||||
|
value={formData.hideCategories}
|
||||||
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Button>Save changes</Button>
|
<Button>Save changes</Button>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Themer from '../Themer/Themer';
|
||||||
import WeatherSettings from './WeatherSettings/WeatherSettings';
|
import WeatherSettings from './WeatherSettings/WeatherSettings';
|
||||||
import OtherSettings from './OtherSettings/OtherSettings';
|
import OtherSettings from './OtherSettings/OtherSettings';
|
||||||
import AppDetails from './AppDetails/AppDetails';
|
import AppDetails from './AppDetails/AppDetails';
|
||||||
|
import StyleSettings from './StyleSettings/StyleSettings';
|
||||||
|
|
||||||
const Settings = (): JSX.Element => {
|
const Settings = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
@ -40,6 +41,13 @@ const Settings = (): JSX.Element => {
|
||||||
to='/settings/other'>
|
to='/settings/other'>
|
||||||
Other
|
Other
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
className={classes.SettingsNavLink}
|
||||||
|
activeClassName={classes.SettingsNavLinkActive}
|
||||||
|
exact
|
||||||
|
to='/settings/css'>
|
||||||
|
CSS
|
||||||
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
className={classes.SettingsNavLink}
|
className={classes.SettingsNavLink}
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
activeClassName={classes.SettingsNavLinkActive}
|
||||||
|
@ -53,6 +61,7 @@ const Settings = (): JSX.Element => {
|
||||||
<Route exact path='/settings' component={Themer} />
|
<Route exact path='/settings' component={Themer} />
|
||||||
<Route path='/settings/weather' component={WeatherSettings} />
|
<Route path='/settings/weather' component={WeatherSettings} />
|
||||||
<Route path='/settings/other' component={OtherSettings} />
|
<Route path='/settings/other' component={OtherSettings} />
|
||||||
|
<Route path='/settings/css' component={StyleSettings} />
|
||||||
<Route path='/settings/app' component={AppDetails} />
|
<Route path='/settings/app' component={AppDetails} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -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<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get<ApiResponse<string>>('/api/config/0/css')
|
||||||
|
.then(data => setCustomStyles(data.data.data))
|
||||||
|
.catch(err => console.log(err.response));
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const inputChangeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setCustomStyles(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSubmitHandler = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
axios.put<ApiResponse<{}>>('/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 (
|
||||||
|
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor='customStyles'>Custom CSS</label>
|
||||||
|
<textarea
|
||||||
|
id='customStyles'
|
||||||
|
name='customStyles'
|
||||||
|
value={customStyles}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
spellCheck={false}
|
||||||
|
></textarea>
|
||||||
|
</InputGroup>
|
||||||
|
<Button>Save CSS</Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, { createNotification })(StyleSettings);
|
|
@ -4,12 +4,14 @@
|
||||||
|
|
||||||
.InputGroup label,
|
.InputGroup label,
|
||||||
.InputGroup span,
|
.InputGroup span,
|
||||||
.InputGroup input {
|
.InputGroup input,
|
||||||
|
.InputGroup textarea {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.InputGroup input,
|
.InputGroup input,
|
||||||
.InputGroup select {
|
.InputGroup select,
|
||||||
|
.InputGroup textarea {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -30,4 +32,9 @@
|
||||||
|
|
||||||
.InputGroup label {
|
.InputGroup label {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.InputGroup textarea {
|
||||||
|
resize: none;
|
||||||
|
height: 50vh;
|
||||||
}
|
}
|
|
@ -10,5 +10,9 @@ export interface SettingsForm {
|
||||||
pinAppsByDefault: number;
|
pinAppsByDefault: number;
|
||||||
pinCategoriesByDefault: number;
|
pinCategoriesByDefault: number;
|
||||||
hideHeader: number;
|
hideHeader: number;
|
||||||
|
hideApps: number;
|
||||||
|
hideCategories: number;
|
||||||
|
hideSearch: number;
|
||||||
useOrdering: string;
|
useOrdering: string;
|
||||||
|
openSameTab: number;
|
||||||
}
|
}
|
5
client/src/interfaces/Query.ts
Normal file
5
client/src/interfaces/Query.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Query {
|
||||||
|
name: string;
|
||||||
|
prefix: string;
|
||||||
|
template: string;
|
||||||
|
}
|
|
@ -7,4 +7,5 @@ export * from './Bookmark';
|
||||||
export * from './Category';
|
export * from './Category';
|
||||||
export * from './Notification';
|
export * from './Notification';
|
||||||
export * from './Config';
|
export * from './Config';
|
||||||
export * from './Forms';
|
export * from './Forms';
|
||||||
|
export * from './Query';
|
|
@ -5,11 +5,16 @@ module.exports = function (app) {
|
||||||
target: 'http://localhost:5005'
|
target: 'http://localhost:5005'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const assetsProxy = createProxyMiddleware('/uploads', {
|
||||||
|
target: 'http://localhost:5005'
|
||||||
|
})
|
||||||
|
|
||||||
const wsProxy = createProxyMiddleware('/socket', {
|
const wsProxy = createProxyMiddleware('/socket', {
|
||||||
target: 'http://localhost:5005',
|
target: 'http://localhost:5005',
|
||||||
ws: true
|
ws: true
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(apiProxy);
|
app.use(apiProxy);
|
||||||
|
app.use(assetsProxy);
|
||||||
app.use(wsProxy);
|
app.use(wsProxy);
|
||||||
};
|
};
|
|
@ -61,7 +61,7 @@ export interface AddAppAction {
|
||||||
payload: App;
|
payload: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => {
|
export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
|
const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => {
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `App ${formData.name} added`
|
message: `App added`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ export interface UpdateAppAction {
|
||||||
payload: App;
|
payload: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateApp = (id: number, formData: NewApp) => async (dispatch: Dispatch) => {
|
export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, formData);
|
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, formData);
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `App ${formData.name} updated`
|
message: `App updated`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,5 @@ export * from './iconParser';
|
||||||
export * from './urlParser';
|
export * from './urlParser';
|
||||||
export * from './searchConfig';
|
export * from './searchConfig';
|
||||||
export * from './checkVersion';
|
export * from './checkVersion';
|
||||||
export * from './sortData';
|
export * from './sortData';
|
||||||
|
export * from './searchParser';
|
22
client/src/utility/searchParser.ts
Normal file
22
client/src/utility/searchParser.ts
Normal file
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
client/src/utility/searchQueries.json
Normal file
39
client/src/utility/searchQueries.json
Normal file
|
@ -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="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -14,11 +14,17 @@ exports.createApp = asyncWrapper(async (req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let app;
|
let app;
|
||||||
|
let _body = { ...req.body };
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
_body.icon = req.file.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (pinApps) {
|
if (pinApps) {
|
||||||
if (parseInt(pinApps.value)) {
|
if (parseInt(pinApps.value)) {
|
||||||
app = await App.create({
|
app = await App.create({
|
||||||
...req.body,
|
..._body,
|
||||||
isPinned: true
|
isPinned: true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,6 +2,8 @@ const asyncWrapper = require('../middleware/asyncWrapper');
|
||||||
const ErrorResponse = require('../utils/ErrorResponse');
|
const ErrorResponse = require('../utils/ErrorResponse');
|
||||||
const Config = require('../models/Config');
|
const Config = require('../models/Config');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
const File = require('../utils/File');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
// @desc Insert new key:value pair
|
// @desc Insert new key:value pair
|
||||||
// @route POST /api/config
|
// @route POST /api/config
|
||||||
|
@ -122,6 +124,33 @@ exports.deletePair = asyncWrapper(async (req, res, next) => {
|
||||||
|
|
||||||
await pair.destroy();
|
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({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {}
|
data: {}
|
||||||
|
|
20
db.js
20
db.js
|
@ -1,24 +1,32 @@
|
||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
|
const Logger = require('./utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
const sequelize = new Sequelize({
|
const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: './data/db.sqlite',
|
storage: './data/db.sqlite',
|
||||||
logging: false
|
logging: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const connectDB = async () => {
|
const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log('Connected to database');
|
logger.log('Connected to database');
|
||||||
|
|
||||||
await sequelize.sync({ alter: true });
|
const syncModels = true;
|
||||||
console.log('All models were synced');
|
|
||||||
|
if (syncModels) {
|
||||||
|
logger.log('Starting model synchronization');
|
||||||
|
await sequelize.sync({ alter: true });
|
||||||
|
logger.log('All models were synchronized');
|
||||||
|
}
|
||||||
} catch (error) {
|
} 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 = {
|
module.exports = {
|
||||||
connectDB,
|
connectDB,
|
||||||
sequelize
|
sequelize
|
||||||
};
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
const ErrorResponse = require('../utils/ErrorResponse');
|
const ErrorResponse = require('../utils/ErrorResponse');
|
||||||
const colors = require('colors');
|
const colors = require('colors');
|
||||||
|
const Logger = require('../utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
const errorHandler = (err, req, res, next) => {
|
const errorHandler = (err, req, res, next) => {
|
||||||
let error = { ...err };
|
let error = { ...err };
|
||||||
|
@ -10,8 +12,7 @@ const errorHandler = (err, req, res, next) => {
|
||||||
// error = new ErrorResponse(`Field ${msg}`, 400);
|
// error = new ErrorResponse(`Field ${msg}`, 400);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
console.log(error);
|
logger.log(error.message.split(',')[0], 'ERROR');
|
||||||
console.log(`${err}`);
|
|
||||||
|
|
||||||
res.status(err.statusCode || 500).json({
|
res.status(err.statusCode || 500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
29
middleware/multer.js
Normal file
29
middleware/multer.js
Normal file
|
@ -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');
|
|
@ -2,12 +2,14 @@ const Category = require('./Category');
|
||||||
const Bookmark = require('./Bookmark');
|
const Bookmark = require('./Bookmark');
|
||||||
|
|
||||||
const associateModels = () => {
|
const associateModels = () => {
|
||||||
// Category <> Bookmark
|
|
||||||
Category.hasMany(Bookmark, {
|
Category.hasMany(Bookmark, {
|
||||||
as: 'bookmarks',
|
foreignKey: 'categoryId',
|
||||||
|
as: 'bookmarks'
|
||||||
|
});
|
||||||
|
|
||||||
|
Bookmark.belongsTo(Category, {
|
||||||
foreignKey: 'categoryId'
|
foreignKey: 'categoryId'
|
||||||
});
|
});
|
||||||
Bookmark.belongsTo(Category, { foreignKey: 'categoryId' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = associateModels;
|
module.exports = associateModels;
|
115
package-lock.json
generated
115
package-lock.json
generated
|
@ -224,6 +224,11 @@
|
||||||
"picomatch": "^2.0.4"
|
"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": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||||
|
@ -364,6 +369,43 @@
|
||||||
"fill-range": "^7.0.1"
|
"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": {
|
"bytes": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
"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": {
|
"concurrently": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
"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": {
|
"dot-prop": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"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": {
|
"needle": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||||
},
|
},
|
||||||
|
"streamsearch": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||||
|
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
|
||||||
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||||
|
@ -2577,6 +2682,11 @@
|
||||||
"mime-types": "~2.1.24"
|
"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": {
|
"typedarray-to-buffer": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||||
|
@ -2804,6 +2914,11 @@
|
||||||
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
|
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
|
||||||
"dev": true
|
"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": {
|
"y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"concurrently": "^6.0.2",
|
"concurrently": "^6.0.2",
|
||||||
"dotenv": "^9.0.0",
|
"dotenv": "^9.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"multer": "^1.4.2",
|
||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
"sequelize": "^6.6.2",
|
"sequelize": "^6.6.2",
|
||||||
"sqlite3": "^5.0.2",
|
"sqlite3": "^5.0.2",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const upload = require('../middleware/multer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createApp,
|
createApp,
|
||||||
|
@ -12,7 +13,7 @@ const {
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/')
|
.route('/')
|
||||||
.post(createApp)
|
.post(upload, createApp)
|
||||||
.get(getApps);
|
.get(getApps);
|
||||||
|
|
||||||
router
|
router
|
||||||
|
|
|
@ -8,6 +8,8 @@ const {
|
||||||
updateValue,
|
updateValue,
|
||||||
updateValues,
|
updateValues,
|
||||||
deletePair,
|
deletePair,
|
||||||
|
updateCss,
|
||||||
|
getCss,
|
||||||
} = require('../controllers/config');
|
} = require('../controllers/config');
|
||||||
|
|
||||||
router
|
router
|
||||||
|
@ -22,4 +24,9 @@ router
|
||||||
.put(updateValue)
|
.put(updateValue)
|
||||||
.delete(deletePair);
|
.delete(deletePair);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/0/css')
|
||||||
|
.get(getCss)
|
||||||
|
.put(updateCss);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
30
server.js
30
server.js
|
@ -7,23 +7,25 @@ const Socket = require('./Socket');
|
||||||
const Sockets = require('./Sockets');
|
const Sockets = require('./Sockets');
|
||||||
const associateModels = require('./models/associateModels');
|
const associateModels = require('./models/associateModels');
|
||||||
const initConfig = require('./utils/initConfig');
|
const initConfig = require('./utils/initConfig');
|
||||||
|
const Logger = require('./utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5005;
|
const PORT = process.env.PORT || 5005;
|
||||||
|
|
||||||
connectDB()
|
(async () => {
|
||||||
.then(() => {
|
await connectDB();
|
||||||
associateModels();
|
await associateModels();
|
||||||
initConfig();
|
await initConfig();
|
||||||
});
|
|
||||||
|
|
||||||
// Create server for Express API and WebSockets
|
// Create server for Express API and WebSockets
|
||||||
const server = http.createServer();
|
const server = http.createServer();
|
||||||
server.on('request', api);
|
server.on('request', api);
|
||||||
|
|
||||||
// Register weatherSocket
|
// Register weatherSocket
|
||||||
const weatherSocket = new Socket(server);
|
const weatherSocket = new Socket(server);
|
||||||
Sockets.registerSocket('weather', weatherSocket);
|
Sockets.registerSocket('weather', weatherSocket);
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`);
|
logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`);
|
||||||
})
|
})
|
||||||
|
})();
|
25
utils/File.js
Normal file
25
utils/File.js
Normal file
|
@ -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;
|
|
@ -1,40 +1,39 @@
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
constructor() {
|
log(message, level = 'INFO') {
|
||||||
this.logFileHandler();
|
console.log(`[${this.generateTimestamp()}] [${level}] ${message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logFileHandler() {
|
generateTimestamp() {
|
||||||
if (!fs.existsSync('./flame.log')) {
|
const d = new Date();
|
||||||
fs.writeFileSync('./flame.log', '');
|
|
||||||
} else {
|
// Date
|
||||||
console.log('file exists');
|
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);
|
||||||
|
|
||||||
|
// Timezone
|
||||||
|
const tz = -d.getTimezoneOffset() / 60;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${tz >= 0 ? '+' + tz : tz}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseDate(date, ms = false) {
|
||||||
|
if (ms) {
|
||||||
|
if (date >= 10 && date < 100) {
|
||||||
|
return `0${date}`;
|
||||||
|
} else if (date < 10) {
|
||||||
|
return `00${date}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
writeLog(logMsg, logType) {
|
return date < 10 ? `0${date}` : date.toString();
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseNumber(number) {
|
|
||||||
if (number > 9) {
|
|
||||||
return number;
|
|
||||||
} else {
|
|
||||||
return `0${number}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(logger.generateLog('testMsg', 'INFO'));
|
module.exports = Logger;
|
||||||
|
|
||||||
module.exports = new Logger();
|
|
|
@ -1,5 +1,7 @@
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const Weather = require('../models/Weather');
|
const Weather = require('../models/Weather');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
const clearWeatherData = async () => {
|
const clearWeatherData = async () => {
|
||||||
const weather = await Weather.findOne({
|
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;
|
module.exports = clearWeatherData;
|
|
@ -1,6 +1,8 @@
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const Config = require('../models/Config');
|
const Config = require('../models/Config');
|
||||||
const { config } = require('./initialConfig.json');
|
const { config } = require('./initialConfig.json');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
const initConfig = async () => {
|
const initConfig = async () => {
|
||||||
// Get config values
|
// Get config values
|
||||||
|
@ -26,7 +28,7 @@ const initConfig = async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Initial config created');
|
logger.log('Initial config created');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,22 @@
|
||||||
{
|
{
|
||||||
"key": "useOrdering",
|
"key": "useOrdering",
|
||||||
"value": "createdAt"
|
"value": "createdAt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "openSameTab",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "hideApps",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "hideCategories",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "hideSearch",
|
||||||
|
"value": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -2,15 +2,17 @@ const schedule = require('node-schedule');
|
||||||
const getExternalWeather = require('./getExternalWeather');
|
const getExternalWeather = require('./getExternalWeather');
|
||||||
const clearWeatherData = require('./clearWeatherData');
|
const clearWeatherData = require('./clearWeatherData');
|
||||||
const Sockets = require('../Sockets');
|
const Sockets = require('../Sockets');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
// Update weather data every 15 minutes
|
// Update weather data every 15 minutes
|
||||||
const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => {
|
const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => {
|
||||||
try {
|
try {
|
||||||
const weatherData = await getExternalWeather();
|
const weatherData = await getExternalWeather();
|
||||||
console.log('weather updated');
|
logger.log('Weather updated');
|
||||||
Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData));
|
Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err.message);
|
logger.log(err.message, 'ERROR');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue