Skip to content

Commit

Permalink
feat: search filter&highlight option 추가
Browse files Browse the repository at this point in the history
+ copy&paste 마이너이슈 수정
  • Loading branch information
jin60641 committed Feb 7, 2024
1 parent fdb4a27 commit 90a0ff9
Show file tree
Hide file tree
Showing 21 changed files with 229 additions and 70 deletions.
43 changes: 35 additions & 8 deletions electron/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import axios from 'axios';
import {
app,
BrowserWindow,
clipboard,
dialog,
ipcMain,
Menu,
MenuItemConstructorOptions,
nativeImage,
shell,
} from 'electron';
import fetch from 'electron-fetch';
import * as fs from 'fs';
import * as glob from 'glob';
import https from 'https';
Expand All @@ -26,6 +27,7 @@ import {
searchMusic,
setCount,
} from './apis';
import { urlToBuffer } from './utils';

https.globalAgent.options.rejectUnauthorized = false;
axios.defaults.baseURL = 'https://api.discogs.com';
Expand Down Expand Up @@ -238,17 +240,44 @@ const createWindow = async () => {
});

ipcMain.on('MUSIC.SEARCH_MUSIC', (_event, query) => {
axios.get<any>(`/database/search?q=${encodeURIComponent(query)}&page=1&per_page=5`)
axios.get<{ results?: { cover_image: string, title: string }[] }>(`/database/search?q=${encodeURIComponent(query)}&page=1&per_page=5`)
.then(({ data }) => {
if (data?.results?.length) {
const result = data.results.map((item: any) => ({ picture: item.cover_image }));
const result = data.results.map(({ title, cover_image: picture }) => {
var i = title?.indexOf(' - ');
var [artist, album] = [title.slice(0, i), title.slice(i + 1)];

return {
picture,
artist,
album,
};
});
searchMusic(win, result);
} else {
searchMusic(win, []);
}
}).catch((e) => { console.log(e); });
}).catch(() => {
searchMusic(win, []);
});
});
ipcMain.on('MUSIC.COPY_IMAGE', async (_event, imageUrl: Uint8Array | string) => {
let image;
if (imageUrl instanceof Uint8Array) {
image = nativeImage.createFromBuffer(Buffer.from(imageUrl));
} else if (imageUrl.startsWith('data:')) { // data url
image = nativeImage.createFromDataURL(imageUrl);
} else if (/https?:\/\//.test(imageUrl)) { // external file path (ex. https://your.web/img.jpg)
const buffer = await urlToBuffer(imageUrl);
image = nativeImage.createFromBuffer(buffer);
} else if (fs.existsSync(imageUrl)) { // local file path
image = nativeImage.createFromPath(imageUrl);
}
if (image) {
console.log(image.getSize(), imageUrl);
clipboard.writeImage(image);
}
});

ipcMain.on('MUSIC.SAVE_MUSIC', async (_event, {
filePaths,
metadata: {
Expand Down Expand Up @@ -283,9 +312,7 @@ const createWindow = async () => {
if (image instanceof Uint8Array) {
tags.image = Buffer.from(image);
} else if (/https?:\/\//.test(image) && !fs.existsSync(image)) { // external file path (ex. https://your.web/img.jpg)
const response = await fetch(image);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const buffer = await urlToBuffer(image);
tags.image = buffer;
} else { // local file path (ex. ~/img.jpg) or empty string
tags.image = image;
Expand Down
5 changes: 0 additions & 5 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ import {
clipboard,
contextBridge,
ipcRenderer,
nativeImage,
} from 'electron';

import storage from './storage';

contextBridge.exposeInMainWorld(
'bridge', {
storage,
copyImage: (imgUrl: string) => {
const image = nativeImage.createFromDataURL(imgUrl);
clipboard.writeImage(image);
},
pasteImage: () => {
const image = clipboard.readImage();
return image.toPNG();
Expand Down
8 changes: 8 additions & 0 deletions electron/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import fetch from 'electron-fetch';

export const urlToBuffer = async (url: string) => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return buffer;
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"electron:build": "yarn tsc -p ./electron/tsconfig.json",
"electron:dev": "yarn tsc --watch -p ./electron/tsconfig.json",
"electron": "nodemon --exec \"\"electron .\"\"",
"start": "concurrently \"yarn electron:dev\" \"yarn react:start\" \"yarn electron\"",
"start": "concurrently -k \"yarn electron:dev\" \"yarn react:start\" \"yarn electron\"",
"build": "yarn react:build && yarn electron:build && electron-builder --config \"electron-builder.json\" ",
"lint": "eslint -c .eslintrc.js '{src,electron}/**/*.{ts,tsx}'"
},
Expand Down
6 changes: 3 additions & 3 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import tableActions from 'store/table/actions';
import { RootState } from 'store/types';


const selector = ({ music: { list }, table: { search }, layout: { search: isVisible } }: RootState) => ({
const selector = ({ music: { list }, table: { searchQuery: search }, layout: { search: isVisible } }: RootState) => ({
search,
isVisible,
list,
Expand Down Expand Up @@ -48,7 +48,7 @@ const Search = () => {
const dispatch = useDispatch();
const handleClose = () => {
dispatch(layoutActions.setSearch(false));
dispatch(tableActions.setSearch(''));
dispatch(tableActions.setSearchQuery(''));
};
useEffect(() => {
const handleKeyDownDocument = (e: KeyboardEvent) => {
Expand All @@ -65,7 +65,7 @@ const Search = () => {

const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setIndex(0);
dispatch(tableActions.setSearch(e.target.value));
dispatch(tableActions.setSearchQuery(e.target.value));
};

const handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Drawer/ImageField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const ImageInput: FC = () => {

const handleCopy = useCallback(() => {
if (imgUrl) {
window.bridge.copyImage(imgUrl);
window.bridge.ipc.send(getType(actions.copyImage), imgUrl);
}
handleClose();
}, [handleClose, imgUrl]);
Expand Down Expand Up @@ -296,7 +296,7 @@ const ImageInput: FC = () => {
component='a'
href={imgUrl}
onClick={handleClose}
download
download='download'
>
{t('download')}
</MenuItem>
Expand Down
66 changes: 66 additions & 0 deletions src/pages/Preference/Search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useCallback } from 'react';

import MuiCheckbox from '@material-ui/core/Checkbox';
import MuiFormControlLabel from '@material-ui/core/FormControlLabel';
import { withStyles } from '@material-ui/core/styles';
import { useTranslation } from 'react-i18next';
import {
useDispatch,
useSelector,
} from 'react-redux';

import tableActions from 'store/table/actions';
import { RootState } from 'store/types';

const FormControlLabel = withStyles((theme) => ({
root: {
margin: theme.spacing(-1),
marginBottom: theme.spacing(1),
display: 'flex',
},
}))(MuiFormControlLabel);

const Checkbox = withStyles(() => ({
root: {
width: 36,
height: 36,
},
}))(MuiCheckbox);

const selector = ({ table: { searchSetting } }: RootState) => searchSetting;

const Search: React.FC = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const settings = useSelector(selector);

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const nextSetting = {
...settings,
[e.target.name]: e.target.checked,
};
dispatch(tableActions.setSearchSetting(nextSetting));
}, [dispatch, settings]);

return (
<div>
{Object.entries(settings).map(([key, value]) => (
<FormControlLabel
key={`Preference-Search-${key}`}
control={(
<Checkbox
color='primary'
name={key}
value={value}
checked={value}
onChange={handleChange}
/>
)}
label={t(key)}
/>
))}
</div>
);
};

export default Search;
2 changes: 2 additions & 0 deletions src/pages/Preference/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RootState } from 'store/types';

import Columns from './Columns';
import Language from './Language';
import Search from './Search';
import Theme from './Theme';

const useStyles = makeStyles((theme) => ({
Expand Down Expand Up @@ -47,6 +48,7 @@ const Map: {
[PreferenceState.columns]: Columns,
[PreferenceState.themes]: Theme,
[PreferenceState.language]: Language,
[PreferenceState.search]: Search,
};

const Preference: React.FC = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const Preference: React.FC = () => {
}
}, [list]);

const handleSubmit = useCallback(() => {
const handleSubmit = useCallback(async () => {
dispatch(musicActions.setInputPicture(selected));
handleClose();
}, [selected, dispatch, handleClose]);
Expand Down
42 changes: 42 additions & 0 deletions src/pages/Table/CellText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

import { shallowEqual, useSelector } from 'react-redux';
import { TableCellProps } from 'react-virtualized';
import { v4 as uuid } from 'uuid';

import { RootState } from 'store/types';

type Props = Pick<TableCellProps, 'cellData'>;

const selector = ({ table: { searchQuery, searchSetting: { shouldHighlight } } }: RootState) => ({
searchQuery,
shouldHighlight,
});

export const highlight = (cellData: Props['cellData'], search: string) => {
if (typeof cellData !== 'string' || !search.length) {
return { cellData: '', matched: [], re: '' };
}
const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(escapedSearch, 'gi');
const matched = [...cellData.matchAll(re)];
return { cellData, re, matched };
};

const CellText: React.FC<Props> = ({ cellData = '' }) => {
const { searchQuery: search, shouldHighlight } = useSelector(selector, shallowEqual);
const { cellData: typedCellData, re, matched } = highlight(cellData, search);

if (matched.length === 0 || !shouldHighlight) {
return cellData;
}
return typedCellData.split(re).map((item, i) => i === 0 ? item : (
<React.Fragment key={`${search}-${item}-${uuid()}`}>
<mark>{matched[i - 1][0]}</mark>
{item}
</React.Fragment>
));

};

export default CellText;
24 changes: 2 additions & 22 deletions src/pages/Table/TableBodyCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ import React from 'react';

import { makeStyles } from '@material-ui/core/styles';
import TableCell from '@material-ui/core/TableCell';
import { useSelector } from 'react-redux';
import { TableCellProps } from 'react-virtualized';
import { v4 as uuid } from 'uuid';

import { RootState } from 'store/types';


const selector = ({ table: { search } }: RootState) => search;
import CellText from './CellText';

const useStyles = makeStyles({
tableCell: {
Expand All @@ -25,32 +20,17 @@ interface Props extends TableCellProps {
numeric?: boolean,
}

const highlight = (cellData: string, search: string) => {
const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(escapedSearch, 'gi');
const matched = [...cellData.matchAll(re)];
return cellData.split(re).map((item, i) => i === 0 ? item : (
<React.Fragment key={`${search}-${item}-${uuid()}`}>
<mark>{matched![i - 1][0]}</mark>
{item}
</React.Fragment>
));
};


const TableBodyCell: React.FC<Props> = ({ cellData, numeric }) => {
const search = useSelector(selector);
const classes = useStyles();

const data = (typeof cellData === 'string' && !!search.length) ? highlight(cellData, search) : cellData;
return (
<TableCell
component='div'
className={classes.tableCell}
variant='body'
align={(numeric || false) ? 'right' : 'left'}
>
{data}
<CellText cellData={cellData} />
</TableCell>
);
};
Expand Down
Loading

0 comments on commit 90a0ff9

Please sign in to comment.