mirror of
https://github.com/pawelmalak/flame.git
synced 2026-02-28 17:33:13 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f93659b661 | ||
|
|
88785aaa32 | ||
|
|
4143ae8198 | ||
|
|
f1c48e8a15 | ||
|
|
6445a5009a | ||
|
|
112a35c08f | ||
|
|
7970ac3031 | ||
|
|
c03f302fa6 | ||
|
|
0c3a27febd |
28
README.md
28
README.md
@@ -37,6 +37,9 @@ npm run dev
|
||||
## Installation
|
||||
|
||||
### With Docker (recommended)
|
||||
|
||||
[Docker Hub](https://hub.docker.com/r/pawelmalak/flame)
|
||||
|
||||
#### Building images
|
||||
```sh
|
||||
# build image for amd64 only
|
||||
@@ -56,6 +59,20 @@ docker buildx build \
|
||||
docker run -p 5005:5005 -v /path/to/data:/app/data flame
|
||||
```
|
||||
|
||||
#### Docker-Compose
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
flame:
|
||||
image: pawelmalak/flame:latest
|
||||
container_name: flame
|
||||
volumes:
|
||||
- <host_dir>:/app/data
|
||||
ports:
|
||||
- 5005:5005
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Without Docker
|
||||
Follow instructions from wiki: [Installation without Docker](https://github.com/pawelmalak/flame/wiki/Installation-without-docker)
|
||||
|
||||
@@ -82,7 +99,11 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/
|
||||
|
||||
## Usage
|
||||
### Search bar
|
||||
> While opening links, module will follow `Open all links in the same tab` setting
|
||||
#### Searching
|
||||
To use search bar you need to type your search query with selected prefix. For example, to search for "what is docker" using google search you would type: `/g what is docker`.
|
||||
|
||||
> You can change where to open search results (same/new tab) in the settings
|
||||
|
||||
#### Supported search engines
|
||||
| Name | Prefix | Search URL |
|
||||
|------------|--------|-------------------------------------|
|
||||
@@ -94,7 +115,8 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/
|
||||
| Name | Prefix | Search URL |
|
||||
|--------------------|--------|-----------------------------------------------|
|
||||
| IMDb | /im | https://www.imdb.com/find?q= |
|
||||
| Reddit | /r | -https://www.reddit.com/search?q= |
|
||||
| Reddit | /r | https://www.reddit.com/search?q= |
|
||||
| Spotify | /sp | https://open.spotify.com/search/ |
|
||||
| The Movie Database | /mv | https://www.themoviedb.org/search?query= |
|
||||
| Youtube | /yt | https://www.youtube.com/results?search_query= |
|
||||
|
||||
@@ -124,4 +146,4 @@ Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/
|
||||
## Support
|
||||
If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link:
|
||||
|
||||
[](https://www.paypal.com/paypalme/pawelmalak)
|
||||
[](https://www.paypal.com/paypalme/pawelmalak)
|
||||
|
||||
@@ -1 +1 @@
|
||||
REACT_APP_VERSION=1.5.0
|
||||
REACT_APP_VERSION=1.6.0
|
||||
@@ -16,7 +16,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
href={redirectUrl}
|
||||
target={searchConfig('openSameTab', false) ? '' : '_blank'}
|
||||
target={searchConfig('appsSameTab', false) ? '' : '_blank'}
|
||||
rel='noreferrer'
|
||||
className={classes.AppCard}
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
href={redirectUrl}
|
||||
target={searchConfig('openSameTab', false) ? '' : '_blank'}
|
||||
target={searchConfig('bookmarksSameTab', false) ? '' : '_blank'}
|
||||
rel='noreferrer'
|
||||
key={`bookmark-${bookmark.id}`}>
|
||||
{bookmark.icon && (
|
||||
|
||||
@@ -22,7 +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';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
|
||||
// Functions
|
||||
import { greeter } from './functions/greeter';
|
||||
@@ -89,7 +89,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||
return (
|
||||
<Container>
|
||||
{searchConfig('hideSearch', 0) !== 1
|
||||
? <SearchBox />
|
||||
? <SearchBar />
|
||||
: <div></div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.SearchBox {
|
||||
.SearchBar {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
color: var(--color-primary);
|
||||
@@ -11,7 +11,7 @@
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.SearchBox:focus {
|
||||
.SearchBar:focus {
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
50
client/src/components/SearchBar/SearchBar.tsx
Normal file
50
client/src/components/SearchBar/SearchBar.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useRef, useEffect, KeyboardEvent } from 'react';
|
||||
|
||||
// Redux
|
||||
import { connect } from 'react-redux';
|
||||
import { createNotification } from '../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { NewNotification } from '../../interfaces';
|
||||
|
||||
// CSS
|
||||
import classes from './SearchBar.module.css';
|
||||
|
||||
// Utils
|
||||
import { searchParser } from '../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
}
|
||||
|
||||
const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.focus();
|
||||
}, [])
|
||||
|
||||
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.code === 'Enter') {
|
||||
const prefixFound = searchParser(inputRef.current.value);
|
||||
|
||||
if (!prefixFound) {
|
||||
props.createNotification({
|
||||
title: 'Error',
|
||||
message: 'Prefix not found'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className={classes.SearchBar}
|
||||
onKeyDown={(e) => searchHandler(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(null, { createNotification })(SearchBar);
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createNotification, updateConfig, sortApps, sortCategories } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces';
|
||||
import { GlobalState, NewNotification, Query, SettingsForm } from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||
@@ -16,6 +16,7 @@ import classes from './OtherSettings.module.css';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { queries } from '../../../utility/searchQueries.json';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
@@ -35,8 +36,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
hideApps: 0,
|
||||
hideCategories: 0,
|
||||
hideSearch: 0,
|
||||
defaultSearchProvider: 'd',
|
||||
useOrdering: 'createdAt',
|
||||
openSameTab: 0
|
||||
appsSameTab: 0,
|
||||
bookmarksSameTab: 0,
|
||||
searchSameTab: 0
|
||||
})
|
||||
|
||||
// Get config
|
||||
@@ -49,8 +53,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
hideApps: searchConfig('hideApps', 0),
|
||||
hideCategories: searchConfig('hideCategories', 0),
|
||||
hideSearch: searchConfig('hideSearch', 0),
|
||||
defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'),
|
||||
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
||||
openSameTab: searchConfig('openSameTab', 0)
|
||||
appsSameTab: searchConfig('appsSameTab', 0),
|
||||
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
||||
searchSameTab: searchConfig('searchSameTab', 0)
|
||||
})
|
||||
}, [props.loading]);
|
||||
|
||||
@@ -139,11 +146,46 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='openSameTab'>Open all links in the same tab</label>
|
||||
<label htmlFor='defaultSearchProvider'>Default Search Provider</label>
|
||||
<select
|
||||
id='openSameTab'
|
||||
name='openSameTab'
|
||||
value={formData.openSameTab}
|
||||
id='defaultSearchProvider'
|
||||
name='defaultSearchProvider'
|
||||
value={formData.defaultSearchProvider}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
>
|
||||
{queries.map((query: Query) => (<option value={query.prefix}>{query.name}</option>))}
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='searchSameTab'>Open search results in the same tab</label>
|
||||
<select
|
||||
id='searchSameTab'
|
||||
name='searchSameTab'
|
||||
value={formData.searchSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='appsSameTab'>Open applications in the same tab</label>
|
||||
<select
|
||||
id='appsSameTab'
|
||||
name='appsSameTab'
|
||||
value={formData.appsSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='bookmarksSameTab'>Open bookmarks in the same tab</label>
|
||||
<select
|
||||
id='bookmarksSameTab'
|
||||
name='bookmarksSameTab'
|
||||
value={formData.bookmarksSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
|
||||
@@ -13,6 +13,9 @@ export interface SettingsForm {
|
||||
hideApps: number;
|
||||
hideCategories: number;
|
||||
hideSearch: number;
|
||||
defaultSearchProvider: string;
|
||||
useOrdering: string;
|
||||
openSameTab: number;
|
||||
}
|
||||
appsSameTab: number;
|
||||
bookmarksSameTab: number;
|
||||
searchSameTab: number;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,24 @@ 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));
|
||||
export const searchParser = (searchQuery: string): boolean => {
|
||||
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
||||
const prefix = splitQuery ? splitQuery[1] : searchConfig('defaultSearchProvider', 'd');
|
||||
const search = splitQuery ? encodeURIComponent(splitQuery[2]) : encodeURIComponent(searchQuery);
|
||||
|
||||
const query = queries.find((q: Query) => q.prefix === prefix);
|
||||
|
||||
if (query) {
|
||||
const sameTab = searchConfig('openSameTab', false);
|
||||
const sameTab = searchConfig('searchSameTab', false);
|
||||
|
||||
if (sameTab) {
|
||||
document.location.replace(`${query.template}${search}`);
|
||||
} else {
|
||||
window.open(`${query.template}${search}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"name": "The Movie Database",
|
||||
"prefix": "mv",
|
||||
"template": "https://www.themoviedb.org/search?query="
|
||||
},
|
||||
{
|
||||
"name": "Spotify",
|
||||
"prefix": "sp",
|
||||
"template": "https://open.spotify.com/search/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,8 +2,8 @@ export const urlParser = (url: string): string[] => {
|
||||
let parsedUrl: string;
|
||||
let displayUrl: string;
|
||||
|
||||
if (/https?:\/\//.test(url)) {
|
||||
// Url starts with http[s]:// -> leave it as it is
|
||||
if (/(https?|steam):\/\//.test(url)) {
|
||||
// Url starts with http[s]:// or steam:// -> leave it as it is
|
||||
parsedUrl = url;
|
||||
} else {
|
||||
// No protocol -> apply http:// prefix
|
||||
@@ -11,10 +11,14 @@ export const urlParser = (url: string): string[] => {
|
||||
}
|
||||
|
||||
// Create simplified url to display as text
|
||||
displayUrl = url
|
||||
if (/steam:\/\//.test(url)) {
|
||||
displayUrl = 'Run Steam App';
|
||||
} else {
|
||||
displayUrl = url
|
||||
.replace(/https?:\/\//, '')
|
||||
.replace('www.', '')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
}
|
||||
|
||||
return [displayUrl, parsedUrl]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ const Config = require('../models/Config');
|
||||
const { Op } = require('sequelize');
|
||||
const File = require('../utils/File');
|
||||
const { join } = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// @desc Insert new key:value pair
|
||||
// @route POST /api/config
|
||||
@@ -151,6 +152,9 @@ exports.updateCss = asyncWrapper(async (req, res, next) => {
|
||||
const file = new File(join(__dirname, '../public/flame.css'));
|
||||
file.write(req.body.styles);
|
||||
|
||||
// Copy file to docker volume
|
||||
fs.copyFileSync(join(__dirname, '../public/flame.css'), join(__dirname, '../data/flame.css'));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {}
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
|
||||
if (!fs.existsSync('data/uploads')) {
|
||||
fs.mkdirSync('data/uploads');
|
||||
fs.mkdirSync('data/uploads', { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
|
||||
@@ -7,6 +7,7 @@ const Socket = require('./Socket');
|
||||
const Sockets = require('./Sockets');
|
||||
const associateModels = require('./models/associateModels');
|
||||
const initConfig = require('./utils/initConfig');
|
||||
const findCss = require('./utils/findCss');
|
||||
const Logger = require('./utils/Logger');
|
||||
const logger = new Logger();
|
||||
|
||||
@@ -16,6 +17,7 @@ const PORT = process.env.PORT || 5005;
|
||||
await connectDB();
|
||||
await associateModels();
|
||||
await initConfig();
|
||||
findCss();
|
||||
|
||||
// Create server for Express API and WebSockets
|
||||
const server = http.createServer();
|
||||
|
||||
22
utils/findCss.js
Normal file
22
utils/findCss.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require('fs');
|
||||
const { join } = require('path');
|
||||
const Logger = require('./Logger');
|
||||
const logger = new Logger();
|
||||
|
||||
// Check if flame.css exists in mounted docker volume. Create new file if not
|
||||
const findCss = () => {
|
||||
const srcPath = join(__dirname, '../data/flame.css');
|
||||
const destPath = join(__dirname, '../public/flame.css');
|
||||
|
||||
if (fs.existsSync(srcPath)) {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
logger.log('Custom CSS file found');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Creating empty CSS file');
|
||||
fs.writeFileSync(destPath, '');
|
||||
|
||||
}
|
||||
|
||||
module.exports = findCss;
|
||||
@@ -37,7 +37,15 @@
|
||||
"value": "createdAt"
|
||||
},
|
||||
{
|
||||
"key": "openSameTab",
|
||||
"key": "appsSameTab",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"key": "bookmarksSameTab",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"key": "searchSameTab",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
@@ -51,6 +59,10 @@
|
||||
{
|
||||
"key": "hideSearch",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"key": "defaultSearchProvider",
|
||||
"value": "d"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user