Add option to retrieve workspace icon from the Internet (#117)

This commit is contained in:
Quang Lam 2020-01-18 23:14:53 -06:00 committed by GitHub
parent 96b72a426f
commit b0fb8cb1a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 407 additions and 56 deletions

View file

@ -22,7 +22,7 @@
<link rel="stylesheet" href="{{ '/assets/main.css' | relative_url }}">
<link rel="canonical" href="{{ page.url | replace:'index.html','' | absolute_url }}">
<link rel="shortcut icon" href="{{ '/images/favicon.png' | relative_url }}" type="image/png">
<link rel="icon" href="{{ '/images/favicon.png' | relative_url }}" type="image/png">
<script src="https://kit.fontawesome.com/dd585e114a.js" crossorigin="anonymous"></script>
{% if jekyll.environment == 'production' and site.google_analytics %}
{% include google-analytics.html %}

View file

@ -24,6 +24,7 @@
"repository": "https://github.com/quanglam2807/singlebox",
"author": "Quang Lam <quang.lam2807@gmail.com>",
"dependencies": {
"cheerio": "1.0.0-rc.3",
"download": "7.1.0",
"electron-is-dev": "1.1.0",
"electron-settings": "3.2.0",

View file

@ -0,0 +1,76 @@
const fetch = require('node-fetch');
const cheerio = require('cheerio');
const url = require('url');
const getWebsiteIconUrlAsync = (websiteURL) => fetch(websiteURL)
.then((res) => res.text())
.then((html) => {
const $ = cheerio.load(html);
// rel=apple-touch-icon
// most preferred because it's not transparent
const $appleTouchIcon = $('head > link[rel=apple-touch-icon]');
if ($appleTouchIcon.length > 0) {
// make sure icon is png
if ($appleTouchIcon.attr('type') === 'image/png'
|| $appleTouchIcon.attr('href').endsWith('.png')) {
return url.resolve(websiteURL, $appleTouchIcon.attr('href'));
}
}
// rel=fluid-icon
// https://webmasters.stackexchange.com/questions/23696/whats-the-fluid-icon-meta-tag-for
const $fluidIcon = $('head > link[rel=fluid-icon]');
if ($fluidIcon.length > 100) {
return url.resolve(websiteURL, $fluidIcon.attr('href'));
}
// manifest.json icon
// https://developers.google.com/web/fundamentals/web-app-manifest
const $manifest = $('head > link[rel=manifest]');
if ($('head > link[rel=manifest]').length > 0) {
const manifestUrl = url.resolve(websiteURL, $manifest.attr('href'));
return fetch(manifestUrl)
.then((res) => res.json())
.then((manifestJson) => {
// return icon with largest size
const { icons } = manifestJson;
icons.sort((x, y) => parseInt(x.sizes.split('x'), 10) - parseInt(y.sizes.split('x'), 10));
return url.resolve(websiteURL, icons[icons.length - 1].src);
});
}
// rel=icon
// less preferred because it's not always in high resolution
const $icon = $('head > link[rel=icon]');
if ($icon.length > 0) {
// make sure icon is png
if ($icon.attr('type') === 'image/png'
|| $icon.attr('href').endsWith('.png')) {
return url.resolve(websiteURL, $icon.attr('href'));
}
}
// rel=shortcut icon
// less preferred because it's not always in high resolution
const $shortcutIcon = $('head > link[rel=\'shortcut icon\']');
if ($shortcutIcon.length > 0) {
// make sure icon is png
if ($shortcutIcon.attr('type') === 'image/png'
|| $shortcutIcon.attr('href').endsWith('.png')) {
return url.resolve(websiteURL, $shortcutIcon.attr('href'));
}
}
return undefined;
})
.then((icon) => {
if (!icon) {
// try to get /apple-touch-icon.png
// https://apple.stackexchange.com/questions/172204/how-apple-com-set-apple-touch-icon
const appleTouchIconUrl = url.resolve(websiteURL, '/apple-touch-icon.png');
return fetch(appleTouchIconUrl)
.then((res) => {
if (res.status === 200 && res.headers.get('Content-Type') === 'image/png') return appleTouchIconUrl;
return undefined;
})
.catch(() => undefined);
}
return icon;
});
module.exports = getWebsiteIconUrlAsync;

View file

@ -45,6 +45,9 @@ const {
getPauseNotificationsInfo,
} = require('../libs/notifications');
const sendToAllWindows = require('../libs/send-to-all-windows');
const getWebsiteIconUrlAsync = require('../libs/get-website-icon-url-async');
const createMenu = require('../libs/create-menu');
const aboutWindow = require('../windows/about');
@ -379,6 +382,18 @@ const loadListeners = () => {
global.updateSilent = Boolean(isSilent);
autoUpdater.checkForUpdates();
});
// to be replaced with invoke (electron 7+)
// https://electronjs.org/docs/api/ipc-renderer#ipcrendererinvokechannel-args
ipcMain.on('request-get-website-icon-url', (e, id, url) => {
getWebsiteIconUrlAsync(url)
.then((iconUrl) => {
sendToAllWindows(id, iconUrl);
})
.catch(() => {
sendToAllWindows(id, null);
});
});
};
module.exports = loadListeners;

View file

@ -17,7 +17,7 @@ const create = (id) => {
win = new BrowserWindow({
width: 400,
height: 600,
height: 650,
resizable: false,
maximizable: false,
minimizable: false,

View file

@ -3,12 +3,17 @@ import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import connectComponent from '../../helpers/connect-component';
import isUrl from '../../helpers/is-url';
import getMailtoUrl from '../../helpers/get-mailto-url';
import { updateForm, save } from '../../state/add-workspace/actions';
import {
getIconFromInternet,
save,
updateForm,
} from '../../state/add-workspace/actions';
import defaultIcon from '../../images/default-icon.png';
@ -39,19 +44,25 @@ const styles = (theme) => ({
display: 'flex',
},
avatarLeft: {
padding: theme.spacing.unit,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: 0,
paddingRight: theme.spacing.unit,
},
avatarRight: {
flex: 1,
padding: theme.spacing.unit,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit,
paddingRight: 0,
},
avatar: {
fontFamily: theme.typography.fontFamily,
height: 64,
width: 64,
background: theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black,
background: theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white,
borderRadius: 4,
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black),
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white),
fontSize: '32px',
lineHeight: '64px',
textAlign: 'center',
@ -70,21 +81,27 @@ const styles = (theme) => ({
},
});
const getValidIconPath = (iconPath) => {
const getValidIconPath = (iconPath, internetIcon) => {
if (iconPath) {
if (isUrl(iconPath)) return iconPath;
return `file://${iconPath}`;
}
if (internetIcon) {
return internetIcon;
}
return defaultIcon;
};
const AddWorkspaceCustom = ({
classes,
downloadingIcon,
homeUrl,
homeUrlError,
internetIcon,
isMailApp,
name,
nameError,
onGetIconFromInternet,
onSave,
onUpdateForm,
picturePath,
@ -128,18 +145,20 @@ const AddWorkspaceCustom = ({
<div className={classes.avatarFlex}>
<div className={classes.avatarLeft}>
<div className={classes.avatar}>
<img alt="Icon" className={classes.avatarPicture} src={getValidIconPath(picturePath)} />
<img alt="Icon" className={classes.avatarPicture} src={getValidIconPath(picturePath, internetIcon)} />
</div>
</div>
<div className={classes.avatarRight}>
<Button
variant="contained"
variant="outlined"
size="small"
onClick={() => {
const { remote } = window.require('electron');
const opts = {
properties: ['openFile'],
filters: [
{ name: 'Images', extensions: ['jpg', 'png'] },
{ name: 'PNG (Portable Network Graphics)', extensions: ['png'] },
{ name: 'JPEG (Joint Photographic Experts Group)', extensions: ['jpg', 'jpeg'] },
],
};
remote.dialog.showOpenDialog(remote.getCurrentWindow(), opts)
@ -150,15 +169,29 @@ const AddWorkspaceCustom = ({
});
}}
>
Change Icon
Select Local Image...
</Button>
<Typography variant="caption">
PNG or JPEG.
</Typography>
<Button
variant="outlined"
size="small"
className={classes.buttonBot}
disabled={!homeUrl || homeUrlError || downloadingIcon}
onClick={() => onGetIconFromInternet(true)}
>
{downloadingIcon ? 'Downloading Icon from the Internet...' : 'Download Icon from the Internet'}
</Button>
<br />
<Button
variant="contained"
variant="outlined"
size="small"
className={classes.buttonBot}
onClick={() => onUpdateForm({ picturePath: null })}
onClick={() => onUpdateForm({ picturePath: null, internetIcon: null })}
disabled={!(picturePath || internetIcon)}
>
Remove Icon
Reset to Default
</Button>
</div>
</div>
@ -172,28 +205,34 @@ const AddWorkspaceCustom = ({
);
AddWorkspaceCustom.defaultProps = {
picturePath: null,
homeUrl: '',
homeUrlError: null,
internetIcon: null,
name: '',
nameError: null,
picturePath: null,
};
AddWorkspaceCustom.propTypes = {
classes: PropTypes.object.isRequired,
downloadingIcon: PropTypes.bool.isRequired,
homeUrl: PropTypes.string,
homeUrlError: PropTypes.string,
internetIcon: PropTypes.string,
isMailApp: PropTypes.bool.isRequired,
name: PropTypes.string,
nameError: PropTypes.string,
onGetIconFromInternet: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
onUpdateForm: PropTypes.func.isRequired,
picturePath: PropTypes.string,
};
const mapStateToProps = (state) => ({
downloadingIcon: state.addWorkspace.downloadingIcon,
homeUrl: state.addWorkspace.form.homeUrl,
homeUrlError: state.addWorkspace.form.homeUrlError,
internetIcon: state.addWorkspace.form.internetIcon,
isMailApp: Boolean(getMailtoUrl(state.addWorkspace.form.homeUrl)),
name: state.addWorkspace.form.name,
nameError: state.addWorkspace.form.nameError,
@ -201,8 +240,9 @@ const mapStateToProps = (state) => ({
});
const actionCreators = {
updateForm,
getIconFromInternet,
save,
updateForm,
};
export default connectComponent(

View file

@ -9,14 +9,18 @@ import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Switch from '@material-ui/core/Switch';
import Typography from '@material-ui/core/Typography';
import connectComponent from '../../helpers/connect-component';
import getMailtoUrl from '../../helpers/get-mailto-url';
import defaultIcon from '../../images/default-icon.png';
import { updateForm, save } from '../../state/edit-workspace/actions';
import {
getIconFromInternet,
save,
updateForm,
} from '../../state/edit-workspace/actions';
const styles = (theme) => ({
root: {
@ -43,19 +47,25 @@ const styles = (theme) => ({
display: 'flex',
},
avatarLeft: {
padding: theme.spacing.unit,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: 0,
paddingRight: theme.spacing.unit,
},
avatarRight: {
flex: 1,
padding: theme.spacing.unit,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit,
paddingRight: 0,
},
avatar: {
fontFamily: theme.typography.fontFamily,
height: 64,
width: 64,
background: theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black,
background: theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white,
borderRadius: 4,
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black),
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white),
fontSize: '32px',
lineHeight: '64px',
textAlign: 'center',
@ -74,16 +84,29 @@ const styles = (theme) => ({
},
});
const getValidIconPath = (iconPath, internetIcon) => {
if (iconPath) {
return `file://${iconPath}`;
}
if (internetIcon) {
return internetIcon;
}
return defaultIcon;
};
const EditWorkspace = ({
classes,
disableAudio,
disableNotifications,
downloadingIcon,
hibernateWhenUnused,
homeUrl,
homeUrlError,
internetIcon,
isMailApp,
name,
nameError,
onGetIconFromInternet,
onSave,
onUpdateForm,
picturePath,
@ -124,18 +147,23 @@ const EditWorkspace = ({
<div className={classes.avatarFlex}>
<div className={classes.avatarLeft}>
<div className={classes.avatar}>
<img alt="Icon" className={classes.avatarPicture} src={picturePath ? `file://${picturePath}` : defaultIcon} />
<img
alt="Icon"
className={classes.avatarPicture}
src={getValidIconPath(picturePath, internetIcon)}
/>
</div>
</div>
<div className={classes.avatarRight}>
<Button
variant="contained"
variant="outlined"
size="small"
onClick={() => {
const { remote } = window.require('electron');
const opts = {
properties: ['openFile'],
filters: [
{ name: 'Images', extensions: ['jpg', 'png'] },
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg'] },
],
};
remote.dialog.showOpenDialog(remote.getCurrentWindow(), opts)
@ -146,15 +174,29 @@ const EditWorkspace = ({
});
}}
>
Change Icon
Select Local Image...
</Button>
<Typography variant="caption">
PNG or JPEG.
</Typography>
<Button
variant="outlined"
size="small"
className={classes.buttonBot}
disabled={!homeUrl || homeUrlError || downloadingIcon}
onClick={() => onGetIconFromInternet(true)}
>
{downloadingIcon ? 'Downloading Icon from the Internet...' : 'Download Icon from the Internet'}
</Button>
<br />
<Button
variant="contained"
variant="outlined"
size="small"
className={classes.buttonBot}
onClick={() => onUpdateForm({ picturePath: null })}
onClick={() => onUpdateForm({ picturePath: null, internetIcon: null })}
disabled={!(picturePath || internetIcon)}
>
Remove Icon
Reset to Default
</Button>
</div>
</div>
@ -201,21 +243,25 @@ const EditWorkspace = ({
);
EditWorkspace.defaultProps = {
picturePath: null,
homeUrlError: null,
internetIcon: null,
nameError: null,
picturePath: null,
};
EditWorkspace.propTypes = {
classes: PropTypes.object.isRequired,
disableAudio: PropTypes.bool.isRequired,
disableNotifications: PropTypes.bool.isRequired,
downloadingIcon: PropTypes.bool.isRequired,
hibernateWhenUnused: PropTypes.bool.isRequired,
homeUrl: PropTypes.string.isRequired,
homeUrlError: PropTypes.string,
internetIcon: PropTypes.string,
isMailApp: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
nameError: PropTypes.string,
onGetIconFromInternet: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
onUpdateForm: PropTypes.func.isRequired,
picturePath: PropTypes.string,
@ -224,10 +270,12 @@ EditWorkspace.propTypes = {
const mapStateToProps = (state) => ({
disableAudio: Boolean(state.editWorkspace.form.disableAudio),
disableNotifications: Boolean(state.editWorkspace.form.disableNotifications),
downloadingIcon: state.editWorkspace.downloadingIcon,
hibernateWhenUnused: Boolean(state.editWorkspace.form.hibernateWhenUnused),
homeUrl: state.editWorkspace.form.homeUrl,
homeUrlError: state.editWorkspace.form.homeUrlError,
id: state.editWorkspace.form.id,
internetIcon: state.editWorkspace.form.internetIcon,
isMailApp: Boolean(getMailtoUrl(state.editWorkspace.form.homeUrl)),
name: state.editWorkspace.form.name,
nameError: state.editWorkspace.form.nameError,
@ -236,6 +284,7 @@ const mapStateToProps = (state) => ({
});
const actionCreators = {
getIconFromInternet,
updateForm,
save,
};

View file

@ -35,15 +35,19 @@ const styles = (theme) => ({
avatar: {
height: 32,
width: 32,
background: theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black,
background: theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white,
borderRadius: 4,
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black),
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.black : theme.palette.common.white),
lineHeight: '32px',
textAlign: 'center',
fontWeight: 500,
textTransform: 'uppercase',
boxShadow: theme.shadows[1],
},
addAvatar: {
background: theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black,
color: theme.palette.getContrastText(theme.palette.type === 'dark' ? theme.palette.common.white : theme.palette.common.black),
},
avatarPicture: {
height: 32,
width: 32,
@ -92,7 +96,7 @@ const WorkspaceSelector = ({
onContextMenu={onContextMenu}
tabIndex="0"
>
<div className={classes.avatar}>
<div className={classNames(classes.avatar, id === 'add' && classes.addAvatar)}>
{id !== 'add' ? (
<img alt="Icon" className={classes.avatarPicture} src={picturePath ? `file://${picturePath}` : defaultIcon} draggable={false} />
) : '+'}

View file

@ -12,6 +12,7 @@ export const SET_WORKSPACE = 'SET_WORKSPACE';
// Edit Workspace
export const UPDATE_EDIT_WORKSPACE_FORM = 'UPDATE_EDIT_WORKSPACE_FORM';
export const UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON = 'UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON';
// General
export const UPDATE_CAN_GO_BACK = 'UPDATE_CAN_GO_BACK';
@ -48,9 +49,10 @@ export const ADD_WORKSPACE_GET_REQUEST = 'ADD_WORKSPACE_GET_REQUEST';
export const ADD_WORKSPACE_GET_SUCCESS = 'ADD_WORKSPACE_GET_SUCCESS';
export const ADD_WORKSPACE_RESET = 'ADD_WORKSPACE_RESET';
export const ADD_WORKSPACE_UPDATE_CURRENT_QUERY = 'ADD_WORKSPACE_UPDATE_CURRENT_QUERY';
export const ADD_WORKSPACE_UPDATE_QUERY = 'ADD_WORKSPACE_UPDATE_QUERY';
export const ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON = 'ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON';
export const ADD_WORKSPACE_UPDATE_FORM = 'ADD_WORKSPACE_UPDATE_FORM';
export const ADD_WORKSPACE_UPDATE_MODE = 'ADD_WORKSPACE_UPDATE_MODE';
export const ADD_WORKSPACE_UPDATE_QUERY = 'ADD_WORKSPACE_UPDATE_QUERY';
// License Registration
export const LICENSE_REGISTRATION_FORM_UPDATE = 'LICENSE_REGISTRATION_FORM_UPDATE';

View file

@ -6,9 +6,10 @@ import {
ADD_WORKSPACE_GET_SUCCESS,
ADD_WORKSPACE_RESET,
ADD_WORKSPACE_UPDATE_CURRENT_QUERY,
ADD_WORKSPACE_UPDATE_QUERY,
ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON,
ADD_WORKSPACE_UPDATE_FORM,
ADD_WORKSPACE_UPDATE_MODE,
ADD_WORKSPACE_UPDATE_QUERY,
} from '../../constants/actions';
import validate from '../../helpers/validate';
@ -19,7 +20,7 @@ import { requestCreateWorkspace } from '../../senders';
const client = algoliasearch('OQ55YRVMNP', 'fc0fb115b113c21d58ed6a4b4de1565f');
const index = client.initIndex('apps');
const { remote } = window.require('electron');
const { ipcRenderer, remote } = window.require('electron');
export const getHits = () => (dispatch, getState) => {
const state = getState();
@ -106,11 +107,70 @@ const getValidationRules = () => ({
},
});
export const updateForm = (changes) => ({
type: ADD_WORKSPACE_UPDATE_FORM,
changes: validate(changes, getValidationRules()),
// to be replaced with invoke (electron 7+)
// https://electronjs.org/docs/api/ipc-renderer#ipcrendererinvokechannel-args
export const getWebsiteIconUrlAsync = (url) => new Promise((resolve, reject) => {
try {
const id = Date.now().toString();
ipcRenderer.once(id, (e, uurl) => {
resolve(uurl);
});
ipcRenderer.send('request-get-website-icon-url', id, url);
} catch (err) {
reject(err);
}
});
export const getIconFromInternet = (forceOverwrite) => (dispatch, getState) => {
const { form: { picturePath, homeUrl, homeUrlError } } = getState().addWorkspace;
if ((!forceOverwrite && picturePath) || !homeUrl || homeUrlError) return;
dispatch({
type: ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON,
downloadingIcon: true,
});
getWebsiteIconUrlAsync(homeUrl)
.then((iconUrl) => {
const { form } = getState().addWorkspace;
if (form.homeUrl === homeUrl) {
const changes = { internetIcon: iconUrl || form.internetIcon };
if (forceOverwrite) changes.picturePath = null;
dispatch(({
type: ADD_WORKSPACE_UPDATE_FORM,
changes,
}));
dispatch({
type: ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON,
downloadingIcon: false,
});
}
if (forceOverwrite && !iconUrl) {
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: 'Unable to find a suitable icon from the Internet.',
buttons: ['OK'],
cancelId: 0,
defaultId: 0,
});
}
}).catch(console.log); // eslint-disable-line no-console
};
let timeout2;
export const updateForm = (changes) => (dispatch) => {
dispatch({
type: ADD_WORKSPACE_UPDATE_FORM,
changes: validate(changes, getValidationRules()),
});
clearTimeout(timeout2);
timeout2 = setTimeout(() => {
if (changes.internetIcon === null) return; // user explictly want to get rid of icon
dispatch(getIconFromInternet());
}, 300);
};
export const save = () => (dispatch, getState) => {
const { form } = getState().addWorkspace;
@ -119,7 +179,7 @@ export const save = () => (dispatch, getState) => {
return dispatch(updateForm(validatedChanges));
}
requestCreateWorkspace(form.name, form.homeUrl.trim(), form.picturePath);
requestCreateWorkspace(form.name, form.homeUrl.trim(), form.internetIcon || form.picturePath);
remote.getCurrentWindow().close();
return null;
};

View file

@ -6,9 +6,10 @@ import {
ADD_WORKSPACE_GET_SUCCESS,
ADD_WORKSPACE_RESET,
ADD_WORKSPACE_UPDATE_CURRENT_QUERY,
ADD_WORKSPACE_UPDATE_QUERY,
ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON,
ADD_WORKSPACE_UPDATE_FORM,
ADD_WORKSPACE_UPDATE_MODE,
ADD_WORKSPACE_UPDATE_QUERY,
} from '../../constants/actions';
const hasFailed = (state = false, action) => {
@ -87,14 +88,22 @@ const mode = (state = defaultMode, action) => {
}
};
const downloadingIcon = (state = false, action) => {
switch (action.type) {
case ADD_WORKSPACE_UPDATE_DOWNLOADING_ICON: return action.downloadingIcon;
default: return state;
}
};
export default combineReducers({
currentQuery,
downloadingIcon,
form,
hasFailed,
hits,
isGetting,
mode,
page,
query,
totalPage,
form,
mode,
});

View file

@ -1,5 +1,8 @@
import { UPDATE_EDIT_WORKSPACE_FORM } from '../../constants/actions';
import {
UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON,
UPDATE_EDIT_WORKSPACE_FORM,
} from '../../constants/actions';
import validate from '../../helpers/validate';
import hasErrors from '../../helpers/has-errors';
@ -10,7 +13,7 @@ import {
requestRemoveWorkspacePicture,
} from '../../senders';
const { remote } = window.require('electron');
const { ipcRenderer, remote } = window.require('electron');
const getValidationRules = () => ({
name: {
@ -24,11 +27,63 @@ const getValidationRules = () => ({
},
});
export const updateForm = (changes) => ({
type: UPDATE_EDIT_WORKSPACE_FORM,
changes: validate(changes, getValidationRules()),
// to be replaced with invoke (electron 7+)
// https://electronjs.org/docs/api/ipc-renderer#ipcrendererinvokechannel-args
export const getWebsiteIconUrlAsync = (url) => new Promise((resolve, reject) => {
try {
const id = Date.now().toString();
ipcRenderer.once(id, (e, uurl) => {
resolve(uurl);
});
ipcRenderer.send('request-get-website-icon-url', id, url);
} catch (err) {
reject(err);
}
});
export const getIconFromInternet = (forceOverwrite) => (dispatch, getState) => {
const { form: { picturePath, homeUrl, homeUrlError } } = getState().editWorkspace;
if ((!forceOverwrite && picturePath) || !homeUrl || homeUrlError) return;
dispatch({
type: UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON,
downloadingIcon: true,
});
getWebsiteIconUrlAsync(homeUrl)
.then((iconUrl) => {
const { form } = getState().editWorkspace;
if (form.homeUrl === homeUrl) {
const changes = { internetIcon: iconUrl || form.internetIcon };
if (forceOverwrite) changes.picturePath = null;
dispatch(({
type: UPDATE_EDIT_WORKSPACE_FORM,
changes,
}));
dispatch({
type: UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON,
downloadingIcon: false,
});
}
if (forceOverwrite && !iconUrl) {
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: 'Unable to find a suitable icon from the Internet.',
buttons: ['OK'],
cancelId: 0,
defaultId: 0,
});
}
}).catch(console.log); // eslint-disable-line no-console
};
export const updateForm = (changes) => (dispatch) => {
dispatch({
type: UPDATE_EDIT_WORKSPACE_FORM,
changes: validate(changes, getValidationRules()),
});
};
export const save = () => (dispatch, getState) => {
const { form } = getState().editWorkspace;
@ -53,6 +108,8 @@ export const save = () => (dispatch, getState) => {
if (form.picturePath) {
requestSetWorkspacePicture(id, form.picturePath);
} else if (form.internetIcon) {
requestSetWorkspacePicture(id, form.internetIcon);
} else {
requestRemoveWorkspacePicture(id);
}

View file

@ -1,6 +1,9 @@
import { combineReducers } from 'redux';
import { UPDATE_EDIT_WORKSPACE_FORM } from '../../constants/actions';
import {
UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON,
UPDATE_EDIT_WORKSPACE_FORM,
} from '../../constants/actions';
import { getWorkspaces } from '../../senders';
import getWorkspacesAsList from '../../helpers/get-workspaces-as-list';
@ -28,4 +31,12 @@ const form = (state = defaultForm, action) => {
}
};
export default combineReducers({ form });
const downloadingIcon = (state = false, action) => {
switch (action.type) {
case UPDATE_EDIT_WORKSPACE_DOWNLOADING_ICON: return action.downloadingIcon;
default: return state;
}
};
export default combineReducers({ downloadingIcon, form });

View file

@ -3704,6 +3704,18 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
cheerio@1.0.0-rc.3:
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
dependencies:
css-select "~1.2.0"
dom-serializer "~0.1.1"
entities "~1.1.1"
htmlparser2 "^3.9.1"
lodash "^4.15.0"
parse5 "^3.0.1"
chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -4327,7 +4339,7 @@ css-select-base-adapter@^0.1.1:
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
css-select@^1.1.0:
css-select@^1.1.0, css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@ -4858,6 +4870,14 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-serializer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
domelementtype "^1.3.0"
entities "^1.1.1"
dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
@ -4868,7 +4888,7 @@ domain-browser@^1.1.1:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
domelementtype@1, domelementtype@^1.3.1:
domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@ -5164,7 +5184,7 @@ enhanced-resolve@^4.1.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
entities@^1.1.1:
entities@^1.1.1, entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@ -6696,7 +6716,7 @@ html-webpack-plugin@4.0.0-beta.5:
tapable "^1.1.0"
util.promisify "1.0.0"
htmlparser2@^3.3.0:
htmlparser2@^3.3.0, htmlparser2@^3.9.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@ -8394,7 +8414,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5:
"lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -9545,6 +9565,13 @@ parse5@5.1.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
parse5@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
dependencies:
"@types/node" "*"
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"