1
0
Fork 0
mirror of https://github.com/pawelmalak/flame.git synced 2025-07-19 03:29:37 +02:00
This commit is contained in:
Jackson DaSilva 2023-07-25 09:48:14 +08:00 committed by GitHub
commit ea4a1fe2c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 356 additions and 8 deletions

View file

@ -12,7 +12,7 @@ import { Bookmark, Category } from '../../../interfaces';
// Other // Other
import classes from './BookmarkCard.module.css'; import classes from './BookmarkCard.module.css';
import { Icon } from '../../UI'; import { Icon } from '../../UI';
import { iconParser, isImage, isSvg, isUrl, urlParser } from '../../../utility'; import { iconParser, isImage, isBase64Image, isSvg, isUrl, urlParser } from '../../../utility';
interface Props { interface Props {
category: Category; category: Category;
@ -54,7 +54,18 @@ export const BookmarkCard = (props: Props): JSX.Element => {
if (bookmark.icon) { if (bookmark.icon) {
const { icon, name } = bookmark; const { icon, name } = bookmark;
if (isImage(icon)) { if(isBase64Image(icon)){
iconEl = (
<div className={classes.BookmarkIcon}>
<img
src={icon}
alt={`${name} icon`}
className={classes.CustomIcon}
/>
</div>
);
}
else if (isImage(icon)) {
const source = isUrl(icon) ? icon : `/uploads/${icon}`; const source = isUrl(icon) ? icon : `/uploads/${icon}`;
iconEl = ( iconEl = (

View file

@ -7,6 +7,8 @@ import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store'; import { actionCreators } from '../../store';
import { importBookmark } from '../../store/action-creators';
// Typescript // Typescript
import { Category, Bookmark } from '../../interfaces'; import { Category, Bookmark } from '../../interfaces';
@ -21,6 +23,7 @@ import {
Spinner, Spinner,
Modal, Modal,
Message, Message,
FileButton,
} from '../UI'; } from '../UI';
// Components // Components
@ -49,6 +52,11 @@ export const Bookmarks = (props: Props): JSX.Element => {
const { getCategories, setEditCategory, setEditBookmark } = const { getCategories, setEditCategory, setEditBookmark } =
bindActionCreators(actionCreators, dispatch); bindActionCreators(actionCreators, dispatch);
const { importBookmark } = bindActionCreators(
actionCreators,
dispatch
);
// Load categories if array is empty // Load categories if array is empty
useEffect(() => { useEffect(() => {
if (!categories.length) { if (!categories.length) {
@ -128,6 +136,10 @@ export const Bookmarks = (props: Props): JSX.Element => {
} }
}; };
const importFromFile = async (file: File) => {
await importBookmark({file});
};
const finishEditing = () => { const finishEditing = () => {
setShowTable(false); setShowTable(false);
setEditCategory(null); setEditCategory(null);
@ -169,6 +181,11 @@ export const Bookmarks = (props: Props): JSX.Element => {
handler={finishEditing} handler={finishEditing}
/> />
)} )}
<FileButton
name="Import From File"
icon="mdiPlusBox"
handler={async (f) => await importFromFile(f)}
/>
</div> </div>
)} )}

View file

@ -0,0 +1,31 @@
.ActionButton {
/* background-color: var(--color-accent); */
border: 1.5px solid var(--color-accent);
border-radius: 4px;
color: var(--color-primary);
padding: 5px 15px;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
max-width: 250px;
margin-right: 10px;
margin-bottom: 20px;
}
.ActionButton:hover {
background-color: var(--color-accent);
color: var(--color-background);
cursor: pointer;
}
.ActionButtonIcon {
--size: 20px;
width: var(--size);
height: var(--size);
/* display: flex; */
}
.ActionButtonName {
display: flex;
}

View file

@ -0,0 +1,64 @@
import { ChangeEvent, Fragment, useRef } from 'react';
import { Link } from 'react-router-dom';
import classes from './FileButton.module.css';
import { Icon } from '../..';
interface Props {
name: string;
icon: string;
link?: string;
handler?: (file: File) => void;
}
export const FileButton = (props: Props): JSX.Element => {
const imageRef = useRef<HTMLInputElement>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
let target = e.target as HTMLInputElement;
if (target?.files && target?.files[0]) {
if(props.handler) props.handler(target?.files[0]);
}
};
const open = () => {
imageRef?.current?.click();
};
const body = (
<Fragment>
<div className={classes.ActionButtonIcon}>
<Icon icon={props.icon} />
</div>
<div className={classes.ActionButtonName}>{props.name}</div>
</Fragment>
);
if (props.link) {
return (
<Link to={props.link} tabIndex={0}>
{body}
</Link>
);
} else if (props.handler) {
return (
<div
className={classes.ActionButton}
onClick={open}
tabIndex={0}
>
<input
ref={imageRef}
type="file"
style={{ display: 'none' }}
accept=".html"
onChange={handleChange}
/>
{body}
</div>
);
} else {
return <div className={classes.ActionButton}>{body}</div>;
}
};

View file

@ -14,4 +14,5 @@ export * from './Forms/InputGroup/InputGroup';
export * from './Forms/ModalForm/ModalForm'; export * from './Forms/ModalForm/ModalForm';
export * from './Buttons/ActionButton/ActionButton'; export * from './Buttons/ActionButton/ActionButton';
export * from './Buttons/Button/Button'; export * from './Buttons/Button/Button';
export * from './Buttons/FileButton/FileButton';
export * from './Text/Message/Message'; export * from './Text/Message/Message';

View file

@ -11,3 +11,7 @@ export interface NewBookmark {
export interface Bookmark extends Model, NewBookmark { export interface Bookmark extends Model, NewBookmark {
orderId: number; orderId: number;
} }
export interface BookmarkImport {
file: File
}

View file

@ -6,6 +6,7 @@ import { ActionType } from '../action-types';
import { import {
ApiResponse, ApiResponse,
Bookmark, Bookmark,
BookmarkImport,
Category, Category,
Config, Config,
NewBookmark, NewBookmark,
@ -18,6 +19,7 @@ import {
DeleteBookmarkAction, DeleteBookmarkAction,
DeleteCategoryAction, DeleteCategoryAction,
GetCategoriesAction, GetCategoriesAction,
ImportBookmarkAction,
PinCategoryAction, PinCategoryAction,
ReorderBookmarksAction, ReorderBookmarksAction,
ReorderCategoriesAction, ReorderCategoriesAction,
@ -108,6 +110,55 @@ export const addBookmark =
} }
}; };
export const importBookmark =
(formData: BookmarkImport) =>
async (dispatch: Dispatch<ImportBookmarkAction>) => {
try {
let formd = new FormData();
formd.append("file", formData.file);
const res = await axios.post<ApiResponse<Bookmark>>(
'/api/bookmarks/import',
formd,
{
headers: {
...applyAuth(),
'Content-Type': 'multipart/form-data',
},
}
);
dispatch<any>({
type: ActionType.createNotification,
payload: {
title: 'Success',
message: `Bookmark file imported.`,
},
});
dispatch({
type: ActionType.importBookmark,
payload: res.data.data,
});
// Fetch bookmarks after import.
try {
const res = await axios.get<ApiResponse<Category[]>>('/api/categories', {
headers: applyAuth(),
});
dispatch<any>({
type: ActionType.getCategoriesSuccess,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
} catch (err) {
console.log(err);
}
};
export const pinCategory = export const pinCategory =
(category: Category) => async (dispatch: Dispatch<PinCategoryAction>) => { (category: Category) => async (dispatch: Dispatch<PinCategoryAction>) => {
try { try {

View file

@ -47,6 +47,7 @@ export enum ActionType {
setEditBookmark = 'SET_EDIT_BOOKMARK', setEditBookmark = 'SET_EDIT_BOOKMARK',
reorderBookmarks = 'REORDER_BOOKMARKS', reorderBookmarks = 'REORDER_BOOKMARKS',
sortBookmarks = 'SORT_BOOKMARKS', sortBookmarks = 'SORT_BOOKMARKS',
importBookmark = 'IMPORT_BOOKMARK',
// AUTH // AUTH
login = 'LOGIN', login = 'LOGIN',
logout = 'LOGOUT', logout = 'LOGOUT',

View file

@ -19,6 +19,11 @@ export interface AddBookmarkAction {
payload: Bookmark; payload: Bookmark;
} }
export interface ImportBookmarkAction {
type: ActionType.importBookmark;
payload: Bookmark;
}
export interface PinCategoryAction { export interface PinCategoryAction {
type: ActionType.pinCategory; type: ActionType.pinCategory;
payload: Category; payload: Category;

View file

@ -18,6 +18,10 @@ export const isImage = (data: string): boolean => {
return regex.test(data); return regex.test(data);
}; };
export const isBase64Image = (data: string): boolean => {
return data.startsWith("data:image/")
};
export const isSvg = (data: string): boolean => { export const isSvg = (data: string): boolean => {
const regex = /.(svg)$/i; const regex = /.(svg)$/i;

View file

@ -0,0 +1,22 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const importBookmarkData = require('../../utils/importBookmark');
const Bookmark = require('../../models/Bookmark');
// @desc Import bookmarks from file.
// @route POST /api/bookmarks/import
// @access Public
const importBookmark = asyncWrapper(async (req, res, next) => {
importBookmarkData(req.file.path);
if(next){
next();
} else {
res.status(200).json({
success: true,
data: bookmark,
});
}
});
module.exports = importBookmark;

View file

@ -3,6 +3,7 @@ module.exports = {
getAllBookmarks: require('./getAllBookmarks'), getAllBookmarks: require('./getAllBookmarks'),
getSingleBookmark: require('./getSingleBookmark'), getSingleBookmark: require('./getSingleBookmark'),
updateBookmark: require('./updateBookmark'), updateBookmark: require('./updateBookmark'),
importBookmark: require('./importBookmark'),
deleteBookmark: require('./deleteBookmark'), deleteBookmark: require('./deleteBookmark'),
reorderBookmarks: require('./reorderBookmarks'), reorderBookmarks: require('./reorderBookmarks'),
}; };

View file

@ -14,7 +14,15 @@ const storage = multer.diskStorage({
}, },
}); });
const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml', 'x-icon']; const supportedTypes = [
'jpg',
'jpeg',
'png',
'svg',
'svg+xml',
'x-icon',
'html',
];
const fileFilter = (req, file, cb) => { const fileFilter = (req, file, cb) => {
if (supportedTypes.includes(file.mimetype.split('/')[1])) { if (supportedTypes.includes(file.mimetype.split('/')[1])) {
@ -26,4 +34,7 @@ const fileFilter = (req, file, cb) => {
const upload = multer({ storage, fileFilter }); const upload = multer({ storage, fileFilter });
module.exports = upload.single('icon'); module.exports = {
icon: upload.single('icon'),
bookmark: upload.single('file'),
};

View file

@ -26,6 +26,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"multer": "^1.4.3", "multer": "^1.4.3",
"node-bookmarks-parser": "^2.0.0",
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
"sequelize": "^6.9.0", "sequelize": "^6.9.0",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",

View file

@ -15,13 +15,13 @@ const {
router router
.route('/') .route('/')
.post(auth, requireAuth, upload, createApp) .post(auth, requireAuth, upload.icon, createApp)
.get(auth, getAllApps); .get(auth, getAllApps);
router router
.route('/:id') .route('/:id')
.get(auth, getSingleApp) .get(auth, getSingleApp)
.put(auth, requireAuth, upload, updateApp) .put(auth, requireAuth, upload.icon, updateApp)
.delete(auth, requireAuth, deleteApp); .delete(auth, requireAuth, deleteApp);
router.route('/0/reorder').put(auth, requireAuth, reorderApps); router.route('/0/reorder').put(auth, requireAuth, reorderApps);

View file

@ -11,19 +11,26 @@ const {
updateBookmark, updateBookmark,
deleteBookmark, deleteBookmark,
reorderBookmarks, reorderBookmarks,
importBookmark,
} = require('../controllers/bookmarks'); } = require('../controllers/bookmarks');
const { getAllCategories } = require('../controllers/categories');
router router
.route('/') .route('/')
.post(auth, requireAuth, upload, createBookmark) .post(auth, requireAuth, upload.icon, createBookmark)
.get(auth, getAllBookmarks); .get(auth, getAllBookmarks);
router router
.route('/:id') .route('/:id')
.get(auth, getSingleBookmark) .get(auth, getSingleBookmark)
.put(auth, requireAuth, upload, updateBookmark) .put(auth, requireAuth, upload.icon, updateBookmark)
.delete(auth, requireAuth, deleteBookmark); .delete(auth, requireAuth, deleteBookmark);
router
.route('/import')
.post(auth, requireAuth, upload.bookmark, importBookmark, getAllCategories);
router.route('/0/reorder').put(auth, requireAuth, reorderBookmarks); router.route('/0/reorder').put(auth, requireAuth, reorderBookmarks);
module.exports = router; module.exports = router;

117
utils/importBookmark.js Normal file
View file

@ -0,0 +1,117 @@
const parse = require('node-bookmarks-parser');
var sqlite3 = require('sqlite3');
const File = require('./File');
const databaseFilePath = 'data/db.sqlite';
let bookmarks = [];
function saveBookmarks() {
const db = new sqlite3.Database(databaseFilePath);
const importDate = new Date().toISOString();
db.serialize(() => {
db.all(`SELECT id, name FROM categories`, (err, data) => {
if (err) {
}
for (const bookmark of bookmarks) {
// Found in db.
let stmt = db.prepare(
'INSERT INTO bookmarks (name, url, categoryId, icon, createdAt, updatedAt) VALUES(?,?,?,?,?,?)'
);
stmt.run(
bookmark.title,
bookmark.url,
data.find(
(r) => r.name.toLowerCase() === bookmark.category.toLowerCase()
)?.id,
bookmark.icon,
importDate,
importDate,
(err) => {
if (err) {
console.error(err);
}
}
);
stmt.finalize();
}
});
});
}
function saveCategories() {
return new Promise((resolve, reject) => {
let db = new sqlite3.Database(databaseFilePath);
let uniqueCats = [...new Set(bookmarks.map((r) => r.category))];
const importDate = new Date().toString();
let tasks = {};
db.serialize(() => {
db.all(`SELECT * FROM categories`, (err, data) => {
if (err) {
console.error(err);
return;
}
for (const newCaterory of uniqueCats) {
for (const category of data) {
if (category.name.toLowerCase() === newCaterory.toLowerCase()) {
continue;
}
}
let stmt = db.prepare(
'INSERT INTO categories (name, createdAt, updatedAt) VALUES(?,?,?)'
);
tasks[newCaterory] = false;
stmt.run(newCaterory, importDate, importDate, (err) => {
tasks[newCaterory] = true;
if (err) {
console.error(err);
reject(err);
}
if(Object.keys(tasks).every(function(k){ return tasks[k] })){
resolve();
}
});
stmt.finalize();
}
});
});
});
}
function crawlBookmarks(bookmark, category) {
if (bookmark.type === 'bookmark') {
bookmarks.push({
title: bookmark.title,
url: bookmark.url,
icon: bookmark.icon,
category: category,
});
}
if (bookmark.type === 'folder') {
for (const child of bookmark.children) {
crawlBookmarks(child, bookmark.title);
}
}
}
module.exports = function importBookmark(path) {
const fileContent = new File(path).read();
const bookmarkData = parse(fileContent);
for (const bookmark of bookmarkData) {
crawlBookmarks(bookmark);
}
saveCategories()
.then((r) => {
saveBookmarks();
})
};