Merge conflicts resolved

This commit is contained in:
Manoj Vivek 2020-07-25 15:02:44 +05:30
commit cc491aa21a
58 changed files with 5777 additions and 3841 deletions

View file

@ -208,6 +208,42 @@
"contributions": [
"code"
]
},
{
"login": "kirubakarthikeyan",
"name": "Kiruba Karan",
"avatar_url": "https://avatars0.githubusercontent.com/u/38885946?v=4",
"profile": "https://github.com/kirubakarthikeyan",
"contributions": [
"code"
]
},
{
"login": "sebasrodriguez",
"name": "Sebastián Rodríguez",
"avatar_url": "https://avatars1.githubusercontent.com/u/1605931?v=4",
"profile": "https://github.com/sebasrodriguez",
"contributions": [
"code"
]
},
{
"login": "karthick3018",
"name": "Karthick Raja",
"avatar_url": "https://avatars1.githubusercontent.com/u/47154512?v=4",
"profile": "https://github.com/karthick3018",
"contributions": [
"code"
]
},
{
"login": "jzabala",
"name": "Johnny Zabala",
"avatar_url": "https://avatars0.githubusercontent.com/u/1315054?v=4",
"profile": "https://github.com/jzabala",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 5,

54
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: "CodeQL"
on:
push:
branches: [master, ]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 6 * * 2'
jobs:
analyse:
name: Analyse
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

16
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Lint
on: pull_request
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: hallee/eslint-action@1.0.3
# GITHUB_TOKEN in forked repositories is read-only
# https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
with:
repo-token: ${{secrets.GITHUB_TOKEN}}
source-root: desktop-app

12
.travis.yml Normal file
View file

@ -0,0 +1,12 @@
language: node_js
node_js:
- 12.13.0
os: osx
osx_image: xcode10.2
before_install: cd desktop-app
install:
- brew install yarn
- yarn
- sh add-osx-cert.sh
script:
- yarn run package-mac

View file

@ -1,6 +1,6 @@
# Responsively App [![Twitter Follow](https://img.shields.io/twitter/follow/ResponsivelyApp?style=social)](https://twitter.com/ResponsivelyApp)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Gitter chat](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/responsively-app) [![xscode](https://img.shields.io/badge/Available%20on-xs%3Acode-blue?style=?style=plastic&logo=appveyor&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAAlUlEQVR42uzXSwqAMAwE0Mn9L+3Ggtgkk35QwcnSJo9S+yGwM9DCooCbgn4YrJ4CIPUcQF7/XSBbx2TEz4sAZ2q1RAECBAiYBlCtvwN+KiYAlG7UDGj59MViT9hOwEqAhYCtAsUZvL6I6W8c2wcbd+LIWSCHSTeSAAECngN4xxIDSK9f4B9t377Wd7H5Nt7/Xz8eAgwAvesLRjYYPuUAAAAASUVORK5CYII=)](https://xscode.com/manojvivek/responsively-app) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/manojVivek/responsively-app/issues)
@ -97,6 +97,12 @@ Thanks go to these wonderful people ([emoji key](https://allcontributors.org/doc
<tr>
<td align="center"><a href="https://github.com/diego-vieira"><img src="https://avatars2.githubusercontent.com/u/930792?v=4" width="100px;" alt=""/><br /><sub><b>Diego Vieira</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=diego-vieira" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/pajaydev"><img src="https://avatars0.githubusercontent.com/u/21375014?v=4" width="100px;" alt=""/><br /><sub><b>Ajaykumar</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=pajaydev" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/kirubakarthikeyan"><img src="https://avatars0.githubusercontent.com/u/38885946?v=4" width="100px;" alt=""/><br /><sub><b>Kiruba Karan</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=kirubakarthikeyan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/sebasrodriguez"><img src="https://avatars1.githubusercontent.com/u/1605931?v=4" width="100px;" alt=""/><br /><sub><b>Sebastián Rodríguez</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=sebasrodriguez" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/karthick3018"><img src="https://avatars1.githubusercontent.com/u/47154512?v=4" width="100px;" alt=""/><br /><sub><b>Karthick Raja</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=karthick3018" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jzabala"><img src="https://avatars0.githubusercontent.com/u/1315054?v=4" width="100px;" alt=""/><br /><sub><b>Johnny Zabala</b></sub></a><br /><a href="https://github.com/manojVivek/responsively-app/commits?author=jzabala" title="Code">💻</a></td>
</tr>
</table>

1
desktop-app/.nvmrc Normal file
View file

@ -0,0 +1 @@
12.13.0

View file

@ -5,7 +5,7 @@ matrix:
- os: osx
language: node_js
node_js:
- node
- 12.13.0
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder

View file

@ -0,0 +1,23 @@
#!/usr/bin/env sh
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_OSX_P12 | base64 --decode > $CERTIFICATE_P12
#create a keychain
security create-keychain -p travis $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p travis $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_OSX_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEY_CHAIN
# remove certs
rm -fr *.p12

View file

@ -200,12 +200,6 @@ export function onAddressChange(newURL, force) {
return;
}
const isHashDiff = isHashOnlyChange(newURL, address);
if (isHashDiff) {
return;
}
dispatch(newAddress(newURL));
pubsub.publish(ADDRESS_CHANGE, [{address: newURL, force: false}]);
};

View file

@ -0,0 +1,55 @@
import pubsub from 'pubsub.js';
import type {Dispatch, GetState} from '../reducers/types';
import {
SET_NETWORK_TROTTLING_PROFILE,
CLEAR_NETWORK_CACHE,
} from '../constants/pubsubEvents';
export const CHANGE_ACTIVE_THROTTLING_PROFILE =
'CHANGE_ACTIVE_THROTTLING_PROFILE';
export const SAVE_THROTTLING_PROFILES = 'SAVE_THROTTLING_PROFILES';
export function changeActiveThrottlingProfile(title = 'Online') {
return {
type: CHANGE_ACTIVE_THROTTLING_PROFILE,
title,
};
}
export function saveThrottlingProfilesList(profiles) {
return {
type: SAVE_THROTTLING_PROFILES,
profiles,
};
}
export function onActiveThrottlingProfileChanged(title) {
return (dispatch: Dispatch, getState: GetState) => {
const {
browser: {
networkConfiguration: {throttling},
},
} = getState();
const activeProfile = throttling.find(x => x.title === title);
if (activeProfile != null) {
pubsub.publish(SET_NETWORK_TROTTLING_PROFILE, [activeProfile]);
dispatch(changeActiveThrottlingProfile(title));
}
};
}
export function onThrottlingProfilesListChanged(profiles) {
return (dispatch: Dispatch, getState: GetState) => {
dispatch(saveThrottlingProfilesList(profiles));
const activeProfile = profiles.find(x => x.type === 'Online');
pubsub.publish(SET_NETWORK_TROTTLING_PROFILE, [activeProfile]);
};
}
export function onClearNetworkCache() {
return (dispatch: Dispatch, getState: GetState) => {
pubsub.publish(CLEAR_NETWORK_CACHE);
};
}

View file

@ -9,9 +9,16 @@ import HomePlusIcon from '../icons/HomePlus';
import DeleteCookieIcon from '../icons/DeleteCookie';
import DeleteStorageIcon from '../icons/DeleteStorage';
import {iconsColor, lightIconsColor} from '../../constants/colors';
import {
getExistingSearchResults,
updateExistingUrl,
searchUrlUtils,
} from '../../services/searchUrlSuggestions';
import UrlSearchResults from '../UrlSearchResults';
import commonStyles from '../common.styles.css';
import styles from './style.css';
import debounce from 'lodash/debounce';
type Props = {
address: string,
@ -25,7 +32,24 @@ type State = {
class AddressBar extends React.Component<Props> {
props: Props;
state: State;
constructor(props) {
super(props);
this.state = {
userTypedAddress: props.address,
previousAddress: props.address,
finalUrlResult: null,
canShowSuggestions: false,
};
this.inputRef = React.createRef();
}
componentDidMount() {
document.addEventListener('click', this._handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutside);
}
static getDerivedStateFromProps(props, state) {
if (props.address !== state.previousAddress) {
@ -37,45 +61,17 @@ class AddressBar extends React.Component<Props> {
return null;
}
_handleKeyDown = e => {
if (e.key === 'Enter') {
this.inputRef.current.blur();
this._onChange();
}
};
_normalize = address => {
if (address.indexOf('://') === -1) {
let protocol = 'https://';
if (address.startsWith('localhost') || address.startsWith('127.0.0.1')) {
protocol = 'http://';
}
address = `${protocol}${address}`;
}
return address;
};
_onChange = () => {
if (!this.state.userTypedAddress) {
return;
}
if (this.props.onChange) {
this.props.onChange(this._normalize(this.state.userTypedAddress), true);
}
};
constructor(props) {
super(props);
this.state = {
userTypedAddress: props.address,
previousAddress: props.address,
};
this.inputRef = React.createRef();
}
render() {
return (
<div className={styles.addressBarContainer}>
<div
className={`${styles.addressBarContainer} ${
this.state.finalUrlResult
? this.state.finalUrlResult.length && this.state.canShowSuggestions
? styles.active
: ''
: ''
}`}
>
<input
ref={this.inputRef}
type="text"
@ -85,7 +81,7 @@ class AddressBar extends React.Component<Props> {
placeholder="https://your-website.com"
value={this.state.userTypedAddress}
onKeyDown={this._handleKeyDown}
onChange={e => this.setState({userTypedAddress: e.target.value})}
onChange={this._handleInputChange}
/>
<div className={cx(styles.floatingOptionsContainer)}>
<div
@ -174,9 +170,87 @@ class AddressBar extends React.Component<Props> {
</Tooltip>
</div>
</div>
{this.state.finalUrlResult?.length && this.state.canShowSuggestions ? (
<UrlSearchResults
divClassName={cx(styles.searchBarSuggestionsContainer)}
listItemUiClassName={cx(styles.searchBarSuggestionsListUl)}
listItemsClassName={cx(styles.searchBarSuggestionsListItems)}
filteredSearchResults={this.state.finalUrlResult}
handleUrlChange={this._onSearchedUrlClick}
/>
) : (
''
)}
</div>
);
}
_handleInputChange = e => {
this.setState(
{userTypedAddress: e.target.value, canShowSuggestions: true},
() => {
this._filterExistingUrl();
}
);
};
_handleKeyDown = e => {
if (e.key === 'Enter') {
this.inputRef.current.blur();
this.setState(
{
finalUrlResult: [],
canShowSuggestions: false,
},
() => {
this._onChange();
}
);
}
};
_onChange = () => {
if (!this.state.userTypedAddress) {
return;
}
return (
this.props.onChange &&
this.props.onChange(this._normalize(this.state.userTypedAddress), true)
);
};
_onSearchedUrlClick = (url, index) => {
if (url !== this.state.previousAddress) {
this.props.onChange(this._normalize(url), true);
}
this.setState({
userTypedAddress: url,
finalUrlResult: [],
});
};
_normalize = address => {
if (address.indexOf('://') === -1) {
let protocol = 'https://';
if (address.startsWith('localhost') || address.startsWith('127.0.0.1')) {
protocol = 'http://';
}
address = `${protocol}${address}`;
}
return address;
};
_filterExistingUrl = debounce(() => {
const finalResult = searchUrlUtils(this.state.userTypedAddress);
this.setState({finalUrlResult: finalResult});
}, 300);
_handleClickOutside = () => {
this.setState({
finalUrlResult: [],
});
};
}
export default AddressBar;

View file

@ -7,7 +7,7 @@
padding: 14px 10px;
border-radius: 20px;
background: unset;
color: #ffffffcc;
color: #fffc;
border: solid 1px #ffffff80;
outline: none;
transition: border 500ms ease-out;
@ -25,6 +25,10 @@
text-overflow: ellipsis;
}
.active {
border-radius: 14px 14px 0 0;
}
.addressBarContainer:focus-within {
color: white;
border: solid 1px #7587ec;
@ -35,3 +39,42 @@
display: flex;
right: 5px;
}
.searchBarSuggestionsContainer {
width: calc(100% + 2px);
max-height: 20em;
position: absolute;
left: -1px;
top: 1.8em;
background: #4b4b4c;
border-radius: 0 0 14px 14px;
border-right: solid 1px #7587ec;
border-bottom: solid 1px #7587ec;
border-left: solid 1px #7587ec;
}
.searchBarSuggestionsListItems {
font-size: 1.1em;
line-height: 22px;
color: #cacaca;
padding: 0.4em 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
.searchBarSuggestionsListUl {
padding: 0;
margin: 0;
list-style: none;
}
.searchBarSuggestionsListItems:hover {
background: gray;
color: white;
}
.searchBarSuggestionsListItems.active {
background: gray;
color: white;
}

View file

@ -0,0 +1,27 @@
import React from 'react';
import cx from 'classnames';
import Button from '@material-ui/core/Button';
import CachedIcon from '@material-ui/icons/Cached';
import commonStyles from '../common.styles.css';
export default function ClearNetworkCache(props) {
return (
<div className={cx(commonStyles.sidebarContentSection)}>
<div className={cx(commonStyles.sidebarContentSectionTitleBar)}>
<CachedIcon style={{marginRight: 5}} /> Network Cache
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => props.onClearNetworkCache()}
>
Clear Network Cache
</Button>
</div>
</div>
);
}

View file

@ -5,6 +5,7 @@ import cx from 'classnames';
import DeviceDrawerContainer from '../../containers/DeviceDrawerContainer';
import UserPreferencesContainer from '../../containers/UserPreferencesContainer';
import ExtensionsManagerContainer from '../../containers/ExtensionsManagerContainer';
import NetworkConfigurationContainer from '../../containers/NetworkConfigurationContainer';
import styles from './styles.css';
import commonStyles from '../common.styles.css';
@ -12,6 +13,7 @@ import {
DEVICE_MANAGER,
USER_PREFERENCES,
EXTENSIONS_MANAGER,
NETWORK_CONFIGURATION,
} from '../../constants/DrawerContents';
import {iconsColor} from '../../constants/colors';
import DoubleLeftArrowIcon from '../icons/DoubleLeftArrow';
@ -70,6 +72,8 @@ function getDrawerContent(type) {
return <UserPreferencesContainer />;
case EXTENSIONS_MANAGER:
return <ExtensionsManagerContainer />;
case NETWORK_CONFIGURATION:
return <NetworkConfigurationContainer />;
default:
return null;
}

View file

@ -3,7 +3,7 @@
padding: 20px 0 5px;
background: #252526;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.35);
z-index: 2;
z-index: 5;
}
.darkToast {

View file

@ -6,6 +6,7 @@ import DevicesIcon from '@material-ui/icons/Devices';
import SettingsIcon from '@material-ui/icons/Settings';
import PhotoLibraryIcon from '@material-ui/icons/PhotoLibraryOutlined';
import ExtensionIcon from '@material-ui/icons/Extension';
import NetworkIcon from '../icons/Network';
import cx from 'classnames';
import Logo from '../icons/Logo';
@ -17,6 +18,7 @@ import {
SCREENSHOT_MANAGER,
USER_PREFERENCES,
EXTENSIONS_MANAGER,
NETWORK_CONFIGURATION,
} from '../../constants/DrawerContents';
const LeftIconsPane = props => {
@ -81,6 +83,19 @@ const LeftIconsPane = props => {
<ExtensionIcon {...iconProps} className="extensionsIcon" />
</div>
</Grid>
<Grid
item
className={cx(commonStyles.icons, styles.icon, commonStyles.enabled, {
[commonStyles.selected]:
props.drawer.open &&
props.drawer.content === NETWORK_CONFIGURATION,
})}
onClick={() => toggleState(NETWORK_CONFIGURATION)}
>
<div>
<NetworkIcon {...iconProps} color="white" className="networkIcon" />
</div>
</Grid>
</Grid>
<div style={{position: 'relative'}}>
<div

View file

@ -0,0 +1,25 @@
import React from 'react';
import cx from 'classnames';
import ClearNetworkCache from '../ClearNetworkCache';
import NetworkThrottling from '../NetworkThrottling';
import styles from './styles.css';
import commonStyles from '../common.styles.css';
export default function DeviceDrawer({
throttling,
onActiveThrottlingProfileChanged,
onThrottlingProfilesListChanged,
onClearNetworkCache,
}) {
return (
<div>
<ClearNetworkCache onClearNetworkCache={onClearNetworkCache} />
<NetworkThrottling
throttling={throttling}
onActiveThrottlingProfileChanged={onActiveThrottlingProfileChanged}
onThrottlingProfilesListChanged={onThrottlingProfilesListChanged}
/>
</div>
);
}

View file

@ -0,0 +1,4 @@
.label {
font-size: 14px;
margin-bottom: 5px;
}

View file

@ -0,0 +1,307 @@
import React, {useState} from 'react';
import cx from 'classnames';
import Button from '@material-ui/core/Button';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableFooter from '@material-ui/core/TableFooter';
import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
import AddCircleOutlineOutlinedIcon from '@material-ui/icons/AddCircleOutlineOutlined';
import TextField from '@material-ui/core/TextField';
import NumberFormat from 'react-number-format';
import InputAdornment from '@material-ui/core/InputAdornment';
import commonStyles from '../../common.styles.css';
import styles from './styles.css';
function NumberFormatCustom(props) {
const {inputRef, onChange, ...other} = props;
return (
<NumberFormat
{...other}
getInputRef={inputRef}
onValueChange={values => {
onChange({
target: {
name: props.name,
value: values.floatValue,
},
});
}}
allowNegative={false}
decimalScale={0}
/>
);
}
export default function ProfileManager({profiles, onSave}) {
const [currentProfiles, updateProfiles] = useState(profiles);
const [newElement, setNewElement] = useState({type: 'Custom'});
const [editModeRows, toggleEditModeRows] = useState({});
const newElementIsInvalid =
newElement.title != null &&
(newElement.title.trim() === '' ||
newElement.title.length > 20 ||
currentProfiles.filter(x => x.title === newElement.title).length !== 0);
const addNewElement = () => {
if (!newElementIsInvalid && newElement.title != null) {
setNewElement({type: 'Custom'});
updateProfiles([...currentProfiles, newElement]);
}
};
const removeProfile = title => {
updateProfiles(currentProfiles.filter(p => p.title !== title));
};
const updateProfile = (title, key, value) => {
const profile = currentProfiles.find(x => x.title === title);
if (profile != null && profile.type === 'Custom') {
profile[key] = value;
updateProfiles([...currentProfiles]);
}
};
const toggleEditMode = row => {
if (row.type !== 'Custom') return;
editModeRows[row.title] = !editModeRows[row.title];
toggleEditModeRows({...editModeRows});
};
return (
<div className={cx(styles.profileManagerContainer)}>
<TableContainer className={cx(styles.profilesContainer)}>
<Table size="small">
<TableHead className={cx(styles.profilesHeader)}>
<TableRow>
<TableCell style={{width: '32%'}}>Name</TableCell>
<TableCell style={{width: '21%'}} align="right">
Download
</TableCell>
<TableCell style={{width: '21%'}} align="right">
Upload
</TableCell>
<TableCell style={{width: '21%'}} align="right">
Latency
</TableCell>
<TableCell style={{width: '5%'}} align="right" />
</TableRow>
</TableHead>
<TableBody>
{currentProfiles.map(row => (
<TableRow key={row.title} className={cx(styles.profilesRow)}>
<TableCell
component="th"
scope="row"
className={cx({
[styles.customProfile]: row.type === 'Custom',
})}
onClick={() => toggleEditMode(row)}
>
{row.title}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' && (
<TextField
className={cx(styles.numericField, {
[styles.numericFieldDisabled]:
row.type !== 'Custom' || !editModeRows[row.title],
})}
value={row.downloadKps == null ? '' : row.downloadKps}
onChange={e =>
updateProfile(row.title, 'downloadKps', e.target.value)
}
fullWidth
variant="outlined"
placeholder={
row.type !== 'Custom' || !editModeRows[row.title]
? ''
: '(optional)'
}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">Kb/s</InputAdornment>
),
}}
disabled={
row.type !== 'Custom' || !editModeRows[row.title]
}
/>
)}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' && (
<TextField
className={cx(styles.numericField, {
[styles.numericFieldDisabled]:
row.type !== 'Custom' || !editModeRows[row.title],
})}
value={row.uploadKps == null ? '' : row.uploadKps}
onChange={e =>
updateProfile(row.title, 'uploadKps', e.target.value)
}
fullWidth
variant="outlined"
placeholder={
row.type !== 'Custom' || !editModeRows[row.title]
? ''
: '(optional)'
}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">Kb/s</InputAdornment>
),
}}
disabled={
row.type !== 'Custom' || !editModeRows[row.title]
}
/>
)}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' && (
<TextField
className={cx(styles.numericField, {
[styles.numericFieldDisabled]:
row.type !== 'Custom' || !editModeRows[row.title],
})}
value={row.latencyMs == null ? '' : row.latencyMs}
onChange={e =>
updateProfile(row.title, 'latencyMs', e.target.value)
}
fullWidth
variant="outlined"
placeholder={
row.type !== 'Custom' || !editModeRows[row.title]
? ''
: '(optional)'
}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">ms</InputAdornment>
),
}}
disabled={
row.type !== 'Custom' || !editModeRows[row.title]
}
/>
)}
</TableCell>
<TableCell align="right">
{row.type === 'Custom' && (
<CancelOutlinedIcon
className={cx(styles.actionIcon)}
onClick={() => removeProfile(row.title)}
/>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TableFooter component={Table}>
<TableHead className={cx(styles.profilesHeader)}>
<TableRow>
<TableCell style={{width: '32%'}}>
<TextField
autoFocus
value={newElement.title || ''}
onChange={e =>
setNewElement({...newElement, title: e.target.value})
}
error={newElementIsInvalid}
fullWidth
variant="outlined"
placeholder="New Profile Name"
className={cx(styles.titleField)}
/>
</TableCell>
<TableCell style={{width: '21%'}} align="right">
<TextField
className={cx(styles.numericField)}
value={
newElement.downloadKps == null ? '' : newElement.downloadKps
}
onChange={e =>
setNewElement({...newElement, downloadKps: e.target.value})
}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">Kb/s</InputAdornment>
),
}}
/>
</TableCell>
<TableCell style={{width: '21%'}} align="right">
<TextField
className={cx(styles.numericField)}
value={newElement.uploadKps == null ? '' : newElement.uploadKps}
onChange={e =>
setNewElement({...newElement, uploadKps: e.target.value})
}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">Kb/s</InputAdornment>
),
}}
/>
</TableCell>
<TableCell style={{width: '21%'}} align="right">
<TextField
className={cx(styles.numericField)}
value={newElement.latencyMs == null ? '' : newElement.latencyMs}
onChange={e =>
setNewElement({...newElement, latencyMs: e.target.value})
}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: (
<InputAdornment position="end">ms</InputAdornment>
),
}}
/>
</TableCell>
<TableCell style={{width: '5%'}} align="right">
<AddCircleOutlineOutlinedIcon
className={cx(styles.actionIcon)}
onClick={addNewElement}
/>
</TableCell>
</TableRow>
</TableHead>
</TableFooter>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => onSave(currentProfiles)}
size="large"
className={cx(styles.saveButton)}
>
Save
</Button>
</div>
);
}

View file

@ -0,0 +1,59 @@
.profileManagerContainer {
height: 600px;
padding: 20px;
}
.profilesContainer {
max-height: 430px;
overflow-y: auto;
flex-grow: 1;
}
.profilesRow > th,
td {
padding-right: 16px !important;
}
.profilesRow > th {
cursor: default;
}
.profilesHeader > tr > th {
font-size: 20px;
font-weight: bold;
}
.profilesRow > th.customProfile {
cursor: pointer;
}
.actionIcon {
pointer-events: all;
cursor: pointer;
color: white;
}
.actionIcon:hover {
color: #6075ef;
}
.saveButton {
position: absolute !important;
bottom: 25px;
right: 25px;
}
.numericField * {
text-align: right;
font-size: 14px !important;
}
.numericFieldDisabled fieldset {
border: 0 !important;
}
.numericFieldDisabled input {
color: white !important;
}
.titleField * {
font-size: 14px !important;
}

View file

@ -0,0 +1,134 @@
import React, {useState} from 'react';
import cx from 'classnames';
import Button from '@material-ui/core/Button';
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
import Select from 'react-select';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import ProfileManager from './ProfileManager';
import {makeStyles} from '@material-ui/core/styles';
import commonStyles from '../common.styles.css';
import styles from './styles.css';
const selectStyles = {
control: selectStyles => ({...selectStyles, backgroundColor: '#ffffff10'}),
option: (selectStyles, {data, isDisabled, isFocused, isSelected}) => {
const color = 'white';
return {
...selectStyles,
backgroundColor: isDisabled
? null
: isSelected
? '#ffffff40'
: isFocused
? '#ffffff20'
: null,
color: 'white',
':active': {
...selectStyles[':active'],
backgroundColor: !isDisabled && '#ffffff40',
},
};
},
input: selectStyles => ({...selectStyles}),
placeholder: selectStyles => ({...selectStyles}),
singleValue: (selectStyles, {data}) => ({...selectStyles, color: 'white'}),
menu: selectStyles => ({...selectStyles, background: '#4b4b4b', zIndex: 100}),
};
const useStyles = makeStyles(theme => ({
appBar: {
position: 'relative',
},
title: {
marginLeft: theme.spacing(2),
flex: 1,
},
}));
export default function NetworkThrottling({
throttling,
onActiveThrottlingProfileChanged,
onThrottlingProfilesListChanged,
}) {
const [open, setOpen] = useState(false);
const closeDialog = () => setOpen(false);
const classes = useStyles();
const selectedIdx = throttling.findIndex(p => p.active);
const options = throttling.map(p => ({
value: p.title,
label: p.title,
}));
const selectedOption = options[selectedIdx];
const onThrottlingProfileChanged = val => {
if (val.value !== selectedOption.value)
onActiveThrottlingProfileChanged(val.value);
};
const saveThrottlingProfiles = profiles => {
onThrottlingProfilesListChanged(profiles);
closeDialog();
};
return (
<div className={cx(commonStyles.sidebarContentSection)}>
<div className={cx(commonStyles.sidebarContentSectionTitleBar)}>
<NetworkCheckIcon className={cx(styles.networkThrottlingIcon)} />{' '}
Network Throttling
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<div className={cx(styles.throttlingProfileSelectorContainer)}>
<Select
options={options}
value={selectedOption}
onChange={onThrottlingProfileChanged}
styles={selectStyles}
/>
</div>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => setOpen(true)}
>
Manage Profiles
</Button>
<Dialog
className={cx(styles.profileManagerDialog)}
maxWidth="md"
fullWidth
open={open}
scroll="paper"
onClose={closeDialog}
>
<AppBar className={classes.appBar} color="secondary">
<Toolbar>
<Typography variant="h6" className={classes.title}>
Manage Throttling Profiles
</Typography>
<Button color="inherit" onClick={closeDialog}>
close
</Button>
</Toolbar>
</AppBar>
<DialogContent>
<ProfileManager
profiles={[...throttling]}
onSave={saveThrottlingProfiles}
/>
</DialogContent>
</Dialog>
</div>
</div>
);
}

View file

@ -0,0 +1,6 @@
.networkThrottlingIcon {
margin-right: 5px;
}
.throttlingProfileSelectorContainer {
margin-bottom: 20px;
}

View file

@ -0,0 +1,53 @@
import React, {useState} from 'react';
import TextField from '@material-ui/core/TextField';
import IconButton from '@material-ui/core/IconButton';
import FolderOpenIcon from '@material-ui/icons/FolderOpenOutlined';
import InputAdornment from '@material-ui/core/InputAdornment';
import {ipcRenderer} from 'electron';
import {lightIconsColor} from '../../constants/colors';
import styles from '../UserPreferences/styles.module.css';
import cx from 'classnames';
export default function ScreenShotSavePreference({
onScreenShotSaveLocationChange,
screenShotSavePath,
}) {
const getScreenshotSavePath = async event => {
const screenshotSavePathResponseFromIpc = await ipcRenderer.invoke(
'get-screen-shot-save-path'
);
if (screenshotSavePathResponseFromIpc) {
onScreenShotSaveLocationChange(
'screenShotSavePath',
screenshotSavePathResponseFromIpc
);
}
};
return (
<div className={cx(styles.screenshotLocationInputContainer)}>
<div className={cx(styles.sectionTitle)}>Screenshot Location:</div>
<TextField
type="text"
color="secondary"
id="standard-size-small"
value={screenShotSavePath}
placeholder="Screenshot save path"
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment>
<IconButton
onClick={getScreenshotSavePath}
size="small"
title="Select Screenshots save location"
>
<FolderOpenIcon fontSize="small" htmlColor={lightIconsColor} />
</IconButton>
</InputAdornment>
),
}}
/>
</div>
);
}

View file

@ -22,7 +22,7 @@ const Announcement = () => {
return (
<div className={styles.section}>
<div
className={styles.link}
className={cx(styles.text, styles.link)}
onClick={() => shell.openExternal(data.link)}
>
<span className={cx('featureSuggestionLink', styles.linkText)}>

View file

@ -9,7 +9,7 @@ import Twitter from '../icons/Twitter';
import RoadMap from '../icons/RoadMap';
const Spacer = ({width = 10}) => (
<div className={styles.link} style={{width}} />
<div className={cx(styles.text)} style={{width}} />
);
const AppUpdaterStatusInfoSection = () => {
@ -59,13 +59,13 @@ const StatusBar = ({visible, zoomLevel}) => {
return null;
}
const zoomPercent = Math.round(zoomLevel * 100)
const zoomPercent = Math.round(zoomLevel * 100);
return (
<div className={styles.statusBar}>
<div className={styles.section}>
<div
className={styles.link}
className={cx(styles.text, styles.link)}
onClick={() =>
shell.openExternal('https://github.com/manojVivek/responsively-app')
}
@ -73,7 +73,7 @@ const StatusBar = ({visible, zoomLevel}) => {
<Github width={14} className={styles.linkIcon} />
</div>
<div
className={styles.link}
className={cx(styles.text, styles.link)}
onClick={() =>
shell.openExternal(
'https://twitter.com/intent/follow?original_referer=app&ref_src=twsrc%5Etfw&region=follow_link&screen_name=ResponsivelyApp&tw_p=followbutton'
@ -84,7 +84,7 @@ const StatusBar = ({visible, zoomLevel}) => {
</div>
<Spacer />
<div
className={cx('roadMapLink', styles.link)}
className={cx('roadMapLink', styles.text, styles.link)}
onClick={() =>
shell.openExternal(
'https://github.com/manojVivek/responsively-app/projects/12?fullscreen=true'
@ -96,7 +96,7 @@ const StatusBar = ({visible, zoomLevel}) => {
</div>
<Spacer />
<div
className={styles.link}
className={cx(styles.text, styles.link)}
onClick={() =>
shell.openExternal('https://headwayapp.co/responsively-changelog')
}
@ -105,15 +105,16 @@ const StatusBar = ({visible, zoomLevel}) => {
Changelog
</span>
</div>
<Spacer />
<div className={cx(styles.text)}>
<span className={cx('zoomText', styles.linkText)}>
Zoom: {zoomPercent}%
</span>
</div>
</div>
<AppUpdaterStatusInfoSection />
<div className={styles.section}>
<Announcement />
{zoomPercent !== 100 && <>
<div className={cx('roadMapLink', styles.statusText)}>
{zoomPercent}%
</div>
</>}
</div>
</div>
);

View file

@ -3,7 +3,7 @@
display: flex;
background-color: #ffffff15;
box-shadow: 0 -3px 5px rgba(0, 0, 0, 0.35);
color: #ffffffcc;
color: #fffc;
justify-content: space-between;
}
@ -23,10 +23,10 @@
font-size: 12px;
}
.link {
.text {
display: flex;
align-items: center;
cursor: pointer;
cursor: default;
color: grey;
fill: grey;
border-radius: 2px;
@ -35,6 +35,10 @@
filter: grayscale(1);
}
.link {
cursor: pointer;
}
.link:hover {
color: lightgrey;
fill: lightgrey;

View file

@ -0,0 +1,30 @@
import React from 'react';
const UrlSearchResults = ({
divClassName,
listItemsClassName,
filteredSearchResults,
handleUrlChange,
activeClass,
listItemUiClassName,
}) => (
<div className={divClassName}>
<ul className={listItemUiClassName}>
{filteredSearchResults?.map(
(eachResult, index) =>
index < 8 && (
<li key={index} className={`${listItemsClassName}`}>
<div
onKeyDown={e => handleOnKeyDown(e, eachResult.url)}
onClick={() => handleUrlChange(eachResult.url, index)}
>
{eachResult.url}
</div>
</li>
)
)}
</ul>
</div>
);
export default UrlSearchResults;

View file

@ -9,6 +9,8 @@ import SettingsIcon from '@material-ui/icons/Settings';
import commonStyles from '../common.styles.css';
import styles from './styles.module.css';
import {DEVTOOLS_MODES} from '../../constants/previewerLayouts';
import ScreenShotSavePreference from '../ScreenShotSavePreference/index';
import {userPreferenceSettings} from '../../settings/userPreferenceSettings';
export default function UserPreference({
devToolsConfig,
@ -106,6 +108,15 @@ export default function UserPreference({
/>
</div>
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<ScreenShotSavePreference
screenShotSavePath={
userPreferences.screenShotSavePath ||
userPreferenceSettings.getDefaultScreenshotpath()
}
onScreenShotSaveLocationChange={onChange}
/>
</div>
</div>
);
}

View file

@ -14,3 +14,10 @@
height: 40px;
border: transparent;
}
.screenshotLocationInputContainer {
border-top: 1px solid #383737;
}
.sectionTitle {
margin: 5px 0;
}

View file

@ -4,7 +4,7 @@ import {remote, ipcRenderer} from 'electron';
import cx from 'classnames';
import {Resizable} from 're-resizable';
import {Tooltip} from '@material-ui/core';
import debounce from 'lodash.debounce';
import debounce from 'lodash/debounce';
import pubsub from 'pubsub.js';
import console from 'electron-timber';
import BugIcon from '../icons/Bug';
@ -28,6 +28,8 @@ import {
DELETE_STORAGE,
ADDRESS_CHANGE,
STOP_LOADING,
CLEAR_NETWORK_CACHE,
SET_NETWORK_TROTTLING_PROFILE,
} from '../../constants/pubsubEvents';
import {CAPABILITIES} from '../../constants/devices';
import DevToolsService from '../../services/dev-tools';
@ -44,6 +46,7 @@ import Maximize from '../icons/Maximize';
import Minimize from '../icons/Minimize';
import Focus from '../icons/Focus';
import Unfocus from '../icons/Unfocus';
import {getBrowserSyncEmbedScriptURL} from '../../service/browserSync';
const {BrowserWindow} = remote;
@ -77,6 +80,7 @@ class WebView extends Component {
address: this.props.browser.address,
};
this.subscriptions = [];
this.dbg = null;
}
componentDidMount() {
@ -142,8 +146,20 @@ class WebView extends Component {
)
);
this.webviewRef.current.addEventListener('dom-ready', async () => {
this.subscriptions.push(
pubsub.subscribe(
SET_NETWORK_TROTTLING_PROFILE,
this.setNetworkThrottlingProfile
)
);
this.subscriptions.push(
pubsub.subscribe(CLEAR_NETWORK_CACHE, this.clearNetworkCache)
);
this.webviewRef.current.addEventListener('dom-ready', () => {
this.initEventTriggers(this.webviewRef.current);
this.dbg = this.getWebContents().debugger;
if (!this.dbg.isAttached()) this.dbg.attach();
});
if (this.props.transmitNavigatorStatus) {
@ -249,6 +265,7 @@ class WebView extends Component {
componentWillUnmount() {
this.subscriptions.forEach(pubsub.unsubscribe);
if (this.dbg && this.dbg.isAttached()) this.dbg.detach();
}
initDeviceEmulationParams = () => {
@ -405,6 +422,49 @@ class WebView extends Component {
this.webviewRef.current.send('disableInspectorMessage');
};
setNetworkThrottlingProfile = ({type, downloadKps, uploadKps, latencyMs}) => {
// TODO : change this when https://github.com/electron/electron/issues/21250 is solved
// if (type === 'Online') {
// this.getWebContents().session.disableNetworkEmulation();
// } else if (type === 'Offline') {
// this.getWebContents().session.enableNetworkEmulation({offline: true});
// } else if (type === 'Custom') {
// const downloadThroughput = downloadKps != null? downloadKps * 128 : undefined;
// const uploadThroughput = uploadKps != null? uploadKps * 128 : undefined;
// this.getWebContents().session.enableNetworkEmulation({offline: false, latency: latencyMs, downloadThroughput, uploadThroughput });
// }
// WORKAROUND
if (type === 'Online') {
this.dbg.sendCommand('Network.disable');
} else if (type === 'Offline') {
this.dbg.sendCommand('Network.enable').then(_ => {
this.dbg.sendCommand('Network.emulateNetworkConditions', {
offline: true,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
});
} else {
const downloadThroughput = downloadKps != null ? downloadKps * 128 : -1;
const uploadThroughput = uploadKps != null ? uploadKps * 128 : -1;
const latency = latencyMs || 0;
this.dbg.sendCommand('Network.enable').then(_ => {
this.dbg.sendCommand('Network.emulateNetworkConditions', {
offline: false,
latency,
downloadThroughput,
uploadThroughput,
});
});
}
};
clearNetworkCache = () => {
this.getWebContents().session.clearCache();
};
messageHandler = ({channel: type, args: [message]}) => {
if (type !== MESSAGE_TYPES.toggleEventMirroring && this.state.isUnplugged) {
return;
@ -443,8 +503,19 @@ class WebView extends Component {
pubsub.publish(DISABLE_INSPECTOR_ALL_DEVICES, [message]);
};
initEventTriggers = webview => {
initBrowserSync = webview => {
this.getWebContentForId(webview.getWebContentsId()).executeJavaScript(`
var bsScript= document.createElement('script');
bsScript.src = '${getBrowserSyncEmbedScriptURL()}';
bsScript.async = true;
document.body.appendChild(bsScript);
`);
};
initEventTriggers = webview => {
this.initBrowserSync(webview);
this.getWebContentForId(webview.getWebContentsId()).executeJavaScript(`
responsivelyApp.deviceId = '${this.props.device.id}';
document.addEventListener('mouseleave', () => {
window.responsivelyApp.mouseOn = false;
@ -459,18 +530,6 @@ class WebView extends Component {
}
});
document.addEventListener('scroll', (e) => {
if (!responsivelyApp.mouseOn) {
return;
}
window.responsivelyApp.sendMessageToHost(
'${MESSAGE_TYPES.scroll}',
{
position: {x: window.scrollX, y: window.scrollY},
}
);
});
document.addEventListener(
'click',
(e) => {
@ -494,12 +553,6 @@ class WebView extends Component {
return;
}
e.responsivelyAppProcessed = true;
window.responsivelyApp.sendMessageToHost(
'${MESSAGE_TYPES.click}',
{
cssPath: window.responsivelyApp.cssPath(e.target),
}
);
},
true
);
@ -522,10 +575,19 @@ class WebView extends Component {
};
_flipOrientation = () => {
if (!this.isMobile) return;
if (this.props.sendFlipStatus) {
this.props.sendFlipStatus(!this.state.isTilted);
}
this.setState({isTilted: !this.state.isTilted});
const flippedDeviceDims = {
width: this.state.deviceDimensions.height,
height: this.state.deviceDimensions.width,
};
this.setState({
isTilted: !this.state.isTilted,
deviceDimensions: flippedDeviceDims,
});
};
_unPlug = () => {
@ -552,6 +614,7 @@ class WebView extends Component {
this.getWebContents().setAudioMuted(true);
this.props.onDeviceMutedChange(this.props.device.id, true);
};
_unmuteDevice = () => {
this.getWebContents().setAudioMuted(false);
this.props.onDeviceMutedChange(this.props.device.id, false);
@ -583,17 +646,13 @@ class WebView extends Component {
const {
device: {id, useragent, capabilities},
} = this.props;
const {deviceDimensions, address} = this.state;
const {deviceDimensions, address, isTilted} = this.state;
if (capabilities.includes(CAPABILITIES.responsive)) {
const responsiveStyle = {
width: deviceDimensions.width,
height: deviceDimensions.height,
};
return (
<Resizable
className={cx(styles.resizableView)}
size={{width: responsiveStyle.width, height: responsiveStyle.height}}
size={{width: deviceStyles.width, height: deviceStyles.height}}
onResizeStart={() => {
const updatedTempDims = {
width: deviceDimensions.width,
@ -642,7 +701,7 @@ class WebView extends Component {
className={cx(styles.device)}
src={address || 'about:blank'}
useragent={useragent}
style={responsiveStyle}
style={deviceStyles}
/>
<webview id={`dev-tools-${this.props.key}`} src="about:blank" />
</Resizable>
@ -667,26 +726,25 @@ class WebView extends Component {
render() {
const {
browser: {zoomLevel, previewer},
device: {capabilities},
} = this.props;
const {
isTilted,
deviceDimensions,
errorCode,
errorDesc,
screenshotInProgress,
} = this.state;
const deviceStyles = {
outline: `4px solid ${this.props.browser.userPreferences.deviceOutlineStyle}`,
width:
this.isMobile && isTilted
? deviceDimensions.height
: deviceDimensions.width,
height:
this.isMobile && isTilted
? deviceDimensions.width
: deviceDimensions.height,
outline: `4px solid ${
this._isDevToolsOpen()
? `#6075ef`
: this.props.browser.userPreferences.deviceOutlineStyle
}`,
width: deviceDimensions.width,
height: deviceDimensions.height,
};
const isMuted = this.props.device.isMuted;
const isResponsive = capabilities.includes(CAPABILITIES.responsive);
const shouldMaximize = previewer.layout !== INDIVIDUAL_LAYOUT;
const IconFocus = () => {
if (shouldMaximize)
@ -695,7 +753,9 @@ class WebView extends Component {
};
return (
<div
className={cx(styles.webViewContainer)}
className={cx(styles.webViewContainer, {
[styles.withMarginRight]: isResponsive,
})}
style={{
width: deviceStyles.width * zoomLevel,
height: deviceStyles.height * zoomLevel + 40,
@ -742,21 +802,6 @@ class WebView extends Component {
<DeviceRotateIcon height={17} color={iconsColor} />
</div>
</Tooltip>
<Tooltip title="Disable event mirroring">
<div
className={cx(
styles.webViewToolbarIcons,
commonStyles.icons,
commonStyles.enabled,
{
[commonStyles.selected]: this.state.isUnplugged,
}
)}
onClick={this._unPlug}
>
<UnplugIcon height={30} color={iconsColor} />
</div>
</Tooltip>
<Tooltip
title={isMuted ? 'Unmute' : 'Mute'}
disableFocusListener={true}
@ -799,9 +844,7 @@ class WebView extends Component {
</div>
</div>
<div
className={cx(styles.deviceContainer, {
[styles.devToolsActive]: this._isDevToolsOpen(),
})}
className={cx(styles.deviceContainer)}
style={{
width: deviceStyles.width,
transform: `scale(${zoomLevel})`,

View file

@ -10,6 +10,7 @@ import path from 'path';
import fs from 'fs-extra';
import PromiseWorker from 'promise-worker';
import NotificationMessage from '../NotificationMessage';
import {userPreferenceSettings} from '../../settings/userPreferenceSettings';
const mergeImg = Promise.promisifyAll(_mergeImg);
@ -174,10 +175,11 @@ function _getScreenshotFileName(
.replace(/:/g, '.')
.toUpperCase()}`;
const directoryPath = createSeparateDir ? `${dateString}/` : '';
const userSelectedScreenShotSavePath = userPreferenceSettings.getScreenShotSavePath();
return {
dir: path.join(
os.homedir(),
`Desktop/Responsively-Screenshots`,
userSelectedScreenShotSavePath ||
path.join(os.homedir(), `Desktop/Responsively-Screenshots`),
directoryPath
),
file: `${getWebsiteName(address)} - ${device.name.replace(

View file

@ -15,18 +15,6 @@
display: flex;
}
.webViewToolbarLeft .webViewToolbarIcons {
margin-right: 4px;
}
.webViewToolbarRight {
display: flex;
}
.webViewToolbarRight .webViewToolbarIcons {
margin-left: 4px;
}
.webViewToolbarIcons {
padding: 0 2px;
align-items: center;
@ -36,15 +24,22 @@
width: 30px;
}
.webViewToolbarRight {
display: flex;
}
.webViewToolbarLeft .webViewToolbarIcons {
margin-right: 4px;
}
.webViewToolbarRight .webViewToolbarIcons {
margin-left: 4px;
}
.deviceContainer {
position: relative;
display: inline-flex;
transform-origin: top left;
border: 3px solid #687cee00;
}
.devToolsActive {
border: 3px solid #6075ef;
}
.device {
@ -161,5 +156,9 @@
}
.resizableView {
margin: 0 1rem;
margin: 0 1rem 1rem 0;
}
.withMarginRight {
margin-right: 5rem;
}

View file

@ -0,0 +1,43 @@
import React, {Fragment} from 'react';
export default ({width, height, color, padding, margin}) => (
<Fragment>
<svg
height={height}
width={width}
fill={color}
style={{padding, margin}}
viewBox="0 0 269.393 269.393"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
className="muteIcon"
xmlSpace="preserve"
>
<path
d="M134.696,0C60.424,0,0,60.425,0,134.696s60.424,134.696,134.696,134.696s134.696-60.425,134.696-134.696
S208.968,0,134.696,0z M136.869,252.518c-12.91,0-25.84-8.779-36.409-24.721c-4.421-6.669-8.316-14.428-11.614-22.979h38.351v20.879
c0,4.143,3.358,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-20.879h42.616C173.729,233.559,156.188,252.518,136.869,252.518z M15.242,142.196
H39.23c2.921,11.683,12.5,20.75,24.457,22.928c1.12,8.63,2.689,16.884,4.66,24.693H28.471
C20.961,175.403,16.303,159.28,15.242,142.196z M142.196,17.372c13.801,2.521,26.969,14.745,37.083,34.544
c-3.816,3.294-6.688,7.649-8.15,12.607h-28.934V17.372z M196.607,83.585c-6.375,0-11.563-5.187-11.563-11.563
s5.187-11.563,11.563-11.563s11.563,5.187,11.563,11.563S202.983,83.585,196.607,83.585z M171.13,79.522
c3.01,10.206,11.992,17.875,22.887,18.934c1.574,9.312,2.518,18.938,2.841,28.74h-54.661V79.522H171.13z M127.196,127.196H99.257
c-2.597-10.386-10.455-18.707-20.568-21.958c1.209-8.956,2.981-17.578,5.238-25.716h43.269V127.196z M69.244,150.634
c-8.788,0-15.938-7.149-15.938-15.938s7.149-15.938,15.938-15.938s15.938,7.149,15.938,15.938S78.032,150.634,69.244,150.634z
M63.697,104.267c-11.961,2.175-21.545,11.243-24.467,22.93H15.242c1.063-17.104,5.731-33.247,13.257-47.674H68.42
C66.401,87.448,64.81,95.734,63.697,104.267z M78.7,164.15c10.108-3.253,17.961-11.572,20.557-21.954h27.939v47.621h-43.29
C81.657,181.696,79.906,173.082,78.7,164.15z M142.196,189.817v-47.621h54.667c-0.576,17.011-3.092,33.174-7.111,47.621H142.196z
M211.869,142.196h18.328c4.142,0,7.5-3.357,7.5-7.5s-3.358-7.5-7.5-7.5h-18.331c-0.332-10.757-1.371-21.329-3.116-31.561
c6.386-3.297,11.269-9.105,13.336-16.113h18.809c8.618,16.521,13.499,35.287,13.499,55.174c0,19.866-4.871,38.614-13.471,55.121
h-35.547C209.143,174.965,211.356,158.863,211.869,142.196z M231.602,64.522h-9.517c-3.245-11.005-13.435-19.063-25.477-19.063
c-1.242,0-2.463,0.092-3.66,0.258c-4.421-8.751-9.442-16.33-14.927-22.601C199.615,31.531,218.182,46.041,231.602,64.522z
M127.196,15.242v49.281H88.854c3.211-8.316,6.994-15.876,11.277-22.426c2.267-3.467,1.294-8.115-2.173-10.382
c-3.467-2.268-8.115-1.293-10.382,2.173c-5.761,8.811-10.693,19.19-14.666,30.635H37.791
C58.186,36.436,90.473,17.524,127.196,15.242z M37.752,204.817H72.81c6.236,18.028,14.801,32.962,24.934,43.73
C73.41,240.63,52.477,225.118,37.752,204.817z M178.108,246.242c4.755-5.473,9.176-11.948,13.18-19.369
c3.662-6.787,6.855-14.192,9.587-22.056h30.765C218.235,223.299,199.686,237.815,178.108,246.242z"
/>
</svg>
</Fragment>
);

View file

@ -2,3 +2,4 @@ export const DEVICE_MANAGER = 'DEVICE_MANAGER';
export const SCREENSHOT_MANAGER = 'SCREENSHOT_MANAGER';
export const USER_PREFERENCES = 'USER_PREFERENCES';
export const EXTENSIONS_MANAGER = 'EXTENSIONS_MANAGER';
export const NETWORK_CONFIGURATION = 'NETWORK_CONFIGURATION';

View file

@ -0,0 +1 @@
export const BROWSER_SYNC_VERSION = '2.26.7';

View file

@ -1305,7 +1305,7 @@ export default {
id: '34',
type: 'emulated-device',
device: {
'show-by-default': true,
'show-by-default': false,
title: 'Responsive Mode',
screen: {
horizontal: {
@ -1314,13 +1314,14 @@ export default {
},
'device-pixel-ratio': 2,
vertical: {
width: 790,
height: 500,
width: 500,
height: 790,
},
},
capabilities: ['responsive'],
'user-agent': '',
type: 'notebook',
capabilities: ['responsive', 'mobile'],
'user-agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
type: 'phone',
modes: [
{
title: 'default',

View file

@ -13,5 +13,8 @@ export const ENABLE_INSPECTOR_ALL_DEVICES = 'ENABLE_INSPECTOR_ALL_DEVICES';
export const DISABLE_INSPECTOR_ALL_DEVICES = 'DISABLE_INSPECTOR_ALL_DEVICES';
export const STOP_LOADING = 'STOP_LOADING';
export const SET_NETWORK_TROTTLING_PROFILE = 'SET_NETWORK_TROTTLING_PROFILE';
export const CLEAR_NETWORK_CACHE = 'CLEAR_NETWORK_CACHE';
// status bar events
export const STATUS_BAR_VISIBILITY_CHANGE = 'status-bar-visibility-change';

View file

@ -0,0 +1 @@
export const ADD_SEARCH_RESULTS = 'ADD_SEARCH_RESULTS';

View file

@ -1,5 +1,6 @@
export const ACTIVE_DEVICES = 'activeDevices';
export const CUSTOM_DEVICES = 'customDevices';
export const USER_PREFERENCES = 'userPreferences';
export const NETWORK_CONFIGURATION = 'networkConfiguration';
export const BOOKMARKS = 'bookmarks';
export const STATUS_BAR_VISIBILITY = 'statusBarVisibility';

View file

@ -0,0 +1,23 @@
// @flow
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import NetworkConfiguration from '../../components/NetworkConfiguration';
import * as NetworkConfigActions from '../../actions/networkConfig';
function mapStateToProps(state) {
return {
throttling: state.browser.networkConfiguration.throttling,
// proxy: state.browser.networkConfiguration.proxy,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(NetworkConfigActions, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(NetworkConfiguration);

View file

@ -87,7 +87,11 @@ export default class Root extends Component<Props> {
document.addEventListener('wheel', this.onWheel);
registerShortcut(
{id: 'ZoomIn', title: 'Zoom In', accelerators: ['mod+=', 'mod+shift+=']},
{
id: 'ZoomIn',
title: 'Zoom In',
accelerators: ['mod+=', 'mod++', 'mod+shift+='],
},
() => {
store.dispatch(onZoomChange(store.getState().browser.zoomLevel + 0.1));
},
@ -224,12 +228,16 @@ export default class Root extends Component<Props> {
);
};
onWheel = (e) => {
onWheel = e => {
if (e.ctrlKey) {
const {store} = this.props
store.dispatch(onZoomChange(store.getState().browser.zoomLevel + (e.deltaY < 0 ? 0.1 : -0.1)));
const {store} = this.props;
store.dispatch(
onZoomChange(
store.getState().browser.zoomLevel + (e.deltaY < 0 ? 0.1 : -0.1)
)
);
}
}
};
render() {
const {store, history} = this.props;

View file

@ -39,6 +39,13 @@ import {initMainShortcutManager} from './shortcut-manager/main-shortcut-manager'
import {appUpdater} from './app-updater';
import trimStart from 'lodash/trimStart';
import isURL from 'validator/lib/isURL';
import {
initBrowserSync,
getBrowserSyncHost,
getBrowserSyncEmbedScriptURL,
} from './utils/browserSync';
import {getHostFromURL} from './utils/urlUtils';
import browserSync from 'browser-sync';
const path = require('path');
const chokidar = require('chokidar');
@ -46,9 +53,9 @@ const URL = require('url').URL;
migrateDeviceSchema();
app &&
app.commandLine &&
if (app && app.commandLine) {
app.commandLine.appendSwitch('remote-debugging-port', '31313');
}
if (process.env.NODE_ENV !== 'development') {
Sentry.init({
@ -108,16 +115,14 @@ app.on('open-url', async (event, url) => {
});
app.on('window-all-closed', () => {
if (
false &&
process.env.NODE_ENV === 'development' &&
['win32', 'darwin'].includes(process.platform)
) {
app.removeAsDefaultProtocolClient(protocol);
}
hasActiveWindow = false;
ipcMain.removeAllListeners();
ipcMain.removeHandler('install-extension');
ipcMain.removeHandler('get-local-extension-path');
ipcMain.removeHandler('get-screen-shot-save-path');
ipcMain.removeHandler('request-browser-sync');
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
hasActiveWindow = false;
if (process.platform !== 'darwin') {
app.quit();
}
@ -126,7 +131,10 @@ app.on('window-all-closed', () => {
app.on(
'certificate-error',
(event, webContents, url, error, certificate, callback) => {
if ((settings.get(USER_PREFERENCES) || {}).disableSSLValidation === true) {
if (
getHostFromURL(url) === getBrowserSyncHost() ||
(settings.get(USER_PREFERENCES) || {}).disableSSLValidation === true
) {
event.preventDefault();
callback(true);
}
@ -200,6 +208,7 @@ const openUrl = url => {
const createWindow = async () => {
hasActiveWindow = true;
if (process.env.NODE_ENV === 'development') {
await installExtensions();
}
@ -221,8 +230,6 @@ const createWindow = async () => {
icon: iconPath,
});
ipcMain.removeAllListeners();
mainWindow.loadURL(`file://${__dirname}/app.html`);
mainWindow.webContents.on('did-finish-load', () => {
@ -241,6 +248,8 @@ const createWindow = async () => {
}
});
await initBrowserSync();
initMainShortcutManager();
const onResize = () => {
@ -363,6 +372,24 @@ const createWindow = async () => {
}
});
ipcMain.handle('get-screen-shot-save-path', async event => {
try {
const {filePaths = []} = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
return filePaths[0];
} catch {
return '';
}
});
ipcMain.handle('request-browser-sync', (event, data) => {
const browserSyncOptions = {
url: getBrowserSyncEmbedScriptURL(),
};
return browserSyncOptions;
});
ipcMain.on('open-devtools', (event, ...args) => {
const {webViewId, bounds, mode} = args[0];
if (!webViewId) {

View file

@ -18,6 +18,7 @@ import {
import {appUpdater, AppUpdaterStatus} from './app-updater';
import {statusBarSettings} from './settings/statusBarSettings';
import {STATUS_BAR_VISIBILITY_CHANGE} from './constants/pubsubEvents';
import {userPreferenceSettings} from './settings/userPreferenceSettings';
const path = require('path');
@ -200,10 +201,10 @@ export default class MenuBuilder {
label: 'Open Screenshots folder',
click: () => {
try {
const dir = path.join(
os.homedir(),
`Desktop/Responsively-Screenshots`
);
const userSelectedScreenShotSavePath = userPreferenceSettings.getScreenShotSavePath();
const dir =
userSelectedScreenShotSavePath ||
userPreferenceSettings.getDefaultScreenshotpath();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}

View file

@ -27,6 +27,10 @@ import {
TOGGLE_ALL_DEVICES_MUTED,
TOGGLE_DEVICE_MUTED,
} from '../actions/browser';
import {
CHANGE_ACTIVE_THROTTLING_PROFILE,
SAVE_THROTTLING_PROFILES,
} from '../actions/networkConfig';
import type {Action} from './types';
import getAllDevices from '../constants/devices';
import type {Device} from '../constants/devices';
@ -40,6 +44,7 @@ import {
ACTIVE_DEVICES,
USER_PREFERENCES,
CUSTOM_DEVICES,
NETWORK_CONFIGURATION,
} from '../constants/settingKeys';
import {
getHomepage,
@ -47,6 +52,7 @@ import {
saveHomepage,
saveLastOpenedAddress,
} from '../utils/navigatorUtils';
import {updateExistingUrl} from '../services/searchUrlSuggestions';
export const FILTER_FIELDS = {
OS: 'OS',
@ -112,12 +118,27 @@ type UserPreferenceType = {
drawerState: boolean,
devToolsOpenMode: DevToolsOpenModeType,
deviceOutlineStyle: string,
zoomLevel: number,
};
type FilterFieldType = FILTER_FIELDS.OS | FILTER_FIELDS.DEVICE_TYPE;
type FilterType = {[key: FilterFieldType]: Array<string>};
type NetworkThrottlingProfileType = {
type: 'Online' | 'Offline' | 'Preset' | 'Custom',
title: string,
downloadKps: number,
uploadKps: number,
latencyMs: number,
active: boolean,
};
type NetworkConfigurationType = {
throttling: NetworkThrottlingProfileType[],
// proxy: NetworkProxyProfileType[],
};
export type BrowserStateType = {
devices: Array<Device>,
homepage: string,
@ -135,6 +156,7 @@ export type BrowserStateType = {
isInspecting: boolean,
windowSize: WindowSizeType,
allDevicesMuted: boolean,
networkConfiguration: NetworkConfigurationType,
};
let _activeDevices = null;
@ -229,6 +251,57 @@ function _updateFileWatcher(newURL) {
else ipcRenderer.send('stop-watcher');
}
function getDefaultNetworkThrottlingProfiles(): NetworkThrottlingProfileType[] {
return [
{
type: 'Online',
title: 'Online',
active: true,
},
{
type: 'Offline',
title: 'Offline',
downloadKps: 0,
uploadKps: 0,
latencyMs: 0,
},
// https://github.com/ChromeDevTools/devtools-frontend/blob/4f404fa8beab837367e49f68e29da427361b1f81/front_end/sdk/NetworkManager.js#L251-L265
{
type: 'Preset',
title: 'Slow 3G',
downloadKps: 400,
uploadKps: 400,
latencyMs: 2000,
},
{
type: 'Preset',
title: 'Fast 3G',
downloadKps: 1475,
uploadKps: 675,
latencyMs: 563,
},
];
}
function _getNetworkConfiguration(): NetworkConfigurationType {
const ntwrk: NetworkConfigurationType =
settings.get(NETWORK_CONFIGURATION) || {};
if (ntwrk.throttling == null)
ntwrk.throttling = getDefaultNetworkThrottlingProfiles();
// if (ntwrk.proxy == null)
// ntwrk.proxy = getDefaultNetworkProxyProfiles();
return ntwrk;
}
function _setNetworkConfiguration(
networkConfiguration: NetworkConfigurationType
) {
settings.set(NETWORK_CONFIGURATION, networkConfiguration);
}
export default function browser(
state: BrowserStateType = {
devices: _getActiveDevices(),
@ -237,7 +310,7 @@ export default function browser(
? getLastOpenedAddress()
: getHomepage(),
currentPageMeta: {},
zoomLevel: 0.6,
zoomLevel: _getUserPreferences().zoomLevel || 0.6,
previousZoomLevel: null,
scrollPosition: {x: 0, y: 0},
navigatorStatus: {backEnabled: false, forwardEnabled: false},
@ -269,6 +342,7 @@ export default function browser(
isInspecting: false,
windowSize: getWindowSize(),
allDevicesMuted: false,
networkConfiguration: _getNetworkConfiguration(),
},
action: Action
) {
@ -276,6 +350,7 @@ export default function browser(
case NEW_ADDRESS:
saveLastOpenedAddress(action.address);
_updateFileWatcher(action.address);
updateExistingUrl(action.address);
return {...state, address: action.address, currentPageMeta: {}};
case NEW_PAGE_META_FIELD:
return {
@ -290,6 +365,10 @@ export default function browser(
saveHomepage(homepage);
return {...state, homepage};
case NEW_ZOOM_LEVEL:
_setUserPreferences({
...state.userPreferences,
zoomLevel: action.zoomLevel,
});
return {...state, zoomLevel: action.zoomLevel};
case NEW_SCROLL_POSITION:
return {...state, scrollPosition: action.scrollPosition};
@ -380,6 +459,34 @@ export default function browser(
allDevicesMuted: state.devices.every(x => x.isMuted),
devices: [...state.devices],
};
case CHANGE_ACTIVE_THROTTLING_PROFILE:
const throttling = state.networkConfiguration.throttling;
const activeProfile = throttling.find(x => x.title === action.title);
if (activeProfile != null) {
throttling.forEach(x => (x.active = false));
activeProfile.active = true;
}
return {
...state,
networkConfiguration: {
...state.networkConfiguration,
throttling: [...throttling],
},
};
case SAVE_THROTTLING_PROFILES:
action.profiles.forEach(x => (x.active = false));
action.profiles[0].active = true;
_setNetworkConfiguration({
...state.networkConfiguration,
throttling: action.profiles,
});
return {
...state,
networkConfiguration: {
...state.networkConfiguration,
throttling: action.profiles,
},
};
default:
return state;
}

View file

@ -0,0 +1,18 @@
import {ipcRenderer} from 'electron';
import browserSync from 'browser-sync';
let browserSyncOptions;
initializeBrowserSyncOptions();
async function initializeBrowserSyncOptions() {
if (!browserSyncOptions) {
browserSyncOptions = await ipcRenderer.invoke('request-browser-sync');
}
}
export function getBrowserSyncEmbedScriptURL() {
if (browserSyncOptions) {
return browserSyncOptions.url;
}
}

View file

@ -0,0 +1,78 @@
import React from 'react';
import filter from 'lodash/filter';
import settings from 'electron-settings';
import {ADD_SEARCH_RESULTS} from '../../constants/searchResultSettings';
let previousSearchResults = settings.get(ADD_SEARCH_RESULTS);
export const getExistingSearchResults = () => settings.get(ADD_SEARCH_RESULTS);
const addUrlToSearchResults = url => settings.set(ADD_SEARCH_RESULTS, url);
const deleteSearchResults = () => settings.delete(ADD_SEARCH_RESULTS);
const _sortedExistingUrlSearchResult = filteredData => {
// Most visited site should appear first in the list
filteredData.sort((a, b) => {
if (a.visitedCount > b.visitedCount) {
return -1;
}
if (a.visitedCount < b.visitedCount) {
return 1;
}
return 0;
});
return filteredData;
};
export const searchUrlUtils = url => {
if (url) {
const filteredData = filter(previousSearchResults, eachResult =>
eachResult.url.toLowerCase().includes(url)
);
const finalResult = _sortedExistingUrlSearchResult(filteredData);
return finalResult;
}
return [];
};
const normalizeURL = url => {
if (url.indexOf('?') === -1 && !url.endsWith('/')) {
url = `${url}/`;
}
return url;
};
export const updateExistingUrl = url => {
url = normalizeURL(url);
if (previousSearchResults?.length) {
let updatedSearchResults = [...previousSearchResults];
const index = updatedSearchResults.findIndex(
eachSearchResult => eachSearchResult.url === url
);
if (index !== (undefined || -1 || null)) {
updatedSearchResults[index].visitedCount =
1 + updatedSearchResults[index].visitedCount;
} else {
updatedSearchResults = [
{url, visitedCount: 1},
...updatedSearchResults,
].slice(0, 300);
}
addUrlToSearchResults(updatedSearchResults);
previousSearchResults = updatedSearchResults;
} else {
const addNewUrl = [];
addNewUrl.push({
url,
visitedCount: 1,
});
addUrlToSearchResults(addNewUrl);
previousSearchResults = addNewUrl;
}
return previousSearchResults;
};

View file

@ -0,0 +1,16 @@
import settings from 'electron-settings';
import {USER_PREFERENCES} from '../constants/settingKeys';
import * as os from 'os';
const path = require('path');
class UserPreferenceSettings {
getScreenShotSavePath = () =>
settings.get(USER_PREFERENCES).screenShotSavePath;
getDefaultScreenshotpath = () =>
path.join(os.homedir(), `Desktop/Responsively-Screenshots`);
}
const userPreferenceSettingsInstance = new UserPreferenceSettings();
export {userPreferenceSettingsInstance as userPreferenceSettings};

View file

@ -0,0 +1,37 @@
const browserSync = require('browser-sync').create();
import {BROWSER_SYNC_VERSION} from '../constants/browserSync';
export async function initBrowserSync() {
if (!browserSync.active) {
await initInstance();
}
}
export function getBrowserSyncHost() {
return `localhost:${browserSync.getOption('port')}`;
}
export function getBrowserSyncEmbedScriptURL() {
return `https://${getBrowserSyncHost()}/browser-sync/browser-sync-client.js?v=${BROWSER_SYNC_VERSION}`;
}
async function initInstance(): Promise<> {
return new Promise((resolve, reject) => {
browserSync.init(
{
open: false,
localOnly: true,
https: true,
notify: false,
ui: false,
},
(err, bs) => {
if (err) {
return reject(err);
}
resolve(bs);
}
);
});
}

View file

@ -1,4 +1,5 @@
import settings from 'electron-settings';
import path from 'path';
const HOME_PAGE = 'HOME_PAGE';
const LAST_OPENED_ADDRESS = 'LAST_OPENED_ADDRESS';

View file

@ -0,0 +1,8 @@
export function getHostFromURL(url: String) {
let host = '';
if (url) {
const urlObj = new URL(url);
host = urlObj.host;
}
return host;
}

View file

@ -1563,8 +1563,8 @@ lodash.cond@^4.3.0:
resolved "http://registry.npm.taobao.org/lodash.cond/download/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
lodash@^4.0.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
longest@^1.0.1:
version "1.0.1"

View file

@ -1,7 +1,7 @@
{
"name": "Responsively-App",
"productName": "ResponsivelyApp",
"version": "0.6.1",
"version": "0.8.0",
"description": "A developer-friendly browser for developing responsive web apps",
"scripts": {
"build": "concurrently \"yarn build-main\" \"yarn build-renderer\"",
@ -190,21 +190,21 @@
"@babel/register": "^7.0.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-jest": "^26.1.0",
"babel-loader": "^8.0.4",
"babel-plugin-dev-expression": "^0.2.1",
"babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.20",
"chalk": "^2.4.1",
"concurrently": "^4.1.0",
"concurrently": "^5.2.0",
"connected-react-router": "^6.5.2",
"cross-env": "^5.2.0",
"cross-spawn": "^6.0.5",
"css-loader": "^1.0.1",
"detect-port": "^1.3.0",
"electron": "^8.3.0",
"electron": "^8.4.0",
"electron-builder": "^22.6.1",
"electron-devtools-installer": "^3.0.0",
"electron-devtools-installer": "^3.1.1",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"enzyme-to-json": "^3.3.4",
@ -216,7 +216,7 @@
"eslint-plugin-compat": "^2.6.3",
"eslint-plugin-flowtype": "^3.2.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jest": "^22.0.0",
"eslint-plugin-jest": "^23.18.0",
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
@ -225,10 +225,10 @@
"file-loader": "^2.0.0",
"flow-bin": "^0.77.0",
"flow-runtime": "^0.17.0",
"flow-typed": "^2.5.1",
"flow-typed": "^3.2.0",
"husky": "^1.1.4",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.6.0",
"jest": "^26.1.0",
"lint-staged": "^8.1.0",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.10.0",
@ -240,13 +240,12 @@
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"sinon": "^7.1.1",
"spectron": "^5.0.0",
"style-loader": "^0.23.1",
"stylelint": "^9.8.0",
"stylelint": "^13.6.1",
"stylelint-config-prettier": "^4.0.0",
"stylelint-config-standard": "^18.2.0",
"terser-webpack-plugin": "^1.1.0",
"testcafe": "^0.23.2",
"testcafe": "^1.8.8",
"testcafe-browser-provider-electron": "0.0.11",
"testcafe-live": "^0.1.4",
"testcafe-react-selectors": "^3.0.0",
@ -265,6 +264,7 @@
"@material-ui/lab": "^4.0.0-alpha.26",
"@sentry/electron": "^1.3.0",
"bluebird": "^3.7.2",
"browser-sync": "^2.26.7",
"chokidar": "^3.4.0",
"chrome-remote-interface": "^0.28.2",
"classnames": "^2.2.6",
@ -281,16 +281,17 @@
"flwww": "^2.0.10",
"history": "^4.7.2",
"jimp": "^0.12.1",
"lodash.debounce": "^4.0.8",
"lodash": "^4.17.19",
"merge-img": "^2.1.3",
"mousetrap": "^1.6.5",
"promise-worker": "^2.0.1",
"pubsub.js": "^1.5.2",
"re-resizable": "^6.4.0",
"react": "^16.12.0",
"react": "^16.13.1",
"react-beautiful-dnd": "^11.0.5",
"react-dom": "^16.12.0",
"react-dom": "^16.13.1",
"react-hot-loader": "^4.8",
"react-number-format": "^4.4.1",
"react-redux": "^7.1.0",
"react-resizable": "^1.10.1",
"react-router": "^5.0.1",

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

View file

@ -30,9 +30,10 @@
<meta property="og:title" content="A Web developer's browser - Responsively App" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://responsively.app" />
<meta property="og:description" content="A dev-tool that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/screenshot.png" />
<meta name="twitter:card" content="summary"></meta>
<meta property="og:description" content="A must-have devtool for web developers that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/responsively-image.png" />
<meta name="twitter:image" content="https://responsively.app/assets/img/responsively-image.png"/>
<meta name="twitter:card" content="summary_large_image"></meta>
<meta name="twitter:site" content="@ResponsivelyApp"></meta>
<link
href="assets/css/loaders/loader-typing.css"

View file

@ -31,9 +31,10 @@
<meta property="og:title" content="A Web developer's browser - Responsively App" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://responsively.app" />
<meta property="og:description" content="A dev-tool that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/screenshot.png" />
<meta name="twitter:card" content="summary"></meta>
<meta property="og:description" content="A must-have devtool for web developers that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/responsively-image.png" />
<meta name="twitter:image" content="https://responsively.app/assets/img/responsively-image.png"/>
<meta name="twitter:card" content="summary_large_image"></meta>
<meta name="twitter:site" content="@ResponsivelyApp"></meta>
<link
href="assets/css/loaders/loader-typing.css"

View file

@ -29,9 +29,10 @@
<meta property="og:title" content="A Web developer's browser - Responsively App" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://responsively.app" />
<meta property="og:description" content="A dev-tool that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/screenshot.png" />
<meta name="twitter:card" content="summary"></meta>
<meta property="og:description" content="A must-have devtool for web developers that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/responsively-image.png" />
<meta name="twitter:image" content="https://responsively.app/assets/img/responsively-image.png"/>
<meta name="twitter:card" content="summary_large_image"></meta>
<meta name="twitter:site" content="@ResponsivelyApp"></meta>
<link
href="assets/css/loaders/loader-typing.css"

View file

@ -29,9 +29,10 @@
<meta property="og:title" content="A Web developer's browser - Responsively App" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://responsively.app" />
<meta property="og:description" content="A dev-tool that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/screenshot.png" />
<meta name="twitter:card" content="summary"></meta>
<meta property="og:description" content="A must-have devtool for web developers that aids faster and precise responsive web development." />
<meta property="og:image" content="https://responsively.app/assets/img/responsively-image.png" />
<meta name="twitter:image" content="https://responsively.app/assets/img/responsively-image.png"/>
<meta name="twitter:card" content="summary_large_image"></meta>
<meta name="twitter:site" content="@ResponsivelyApp"></meta>
<link
href="assets/css/loaders/loader-typing.css"