diff --git a/src/main/dialogs/index.ts b/src/main/dialogs/index.ts index fbb3597f0..f42646637 100644 --- a/src/main/dialogs/index.ts +++ b/src/main/dialogs/index.ts @@ -6,3 +6,4 @@ export * from './auth'; export * from './permissions'; export * from './form-fill'; export * from './credentials'; +export * from './preview'; diff --git a/src/main/dialogs/preview.ts b/src/main/dialogs/preview.ts new file mode 100644 index 000000000..eadd9933a --- /dev/null +++ b/src/main/dialogs/preview.ts @@ -0,0 +1,48 @@ +import { AppWindow } from '../windows'; +import { MENU_WIDTH } from '~/constants/design'; +import { Dialog } from '.'; +import { TAB_MAX_WIDTH } from '~/renderer/views/app/constants/tabs'; + +const WIDTH = MENU_WIDTH; +const HEIGHT = 128; + +export class PreviewDialog extends Dialog { + public visible = false; + public tab: { id?: number; x?: number } = {}; + + constructor(appWindow: AppWindow) { + super(appWindow, { + name: 'preview', + bounds: { + width: TAB_MAX_WIDTH + 16, + height: HEIGHT, + y: 40, + }, + hideTimeout: 300, + }); + } + + public show() { + this.bounds.x = Math.round(this.tab.x - 8); + + super.show(); + + const tab = this.appWindow.viewManager.views.find( + x => x.webContents.id === this.tab.id, + ); + + const url = tab.webContents.getURL(); + const title = tab.webContents.getTitle(); + + this.webContents.send('visible', true, { + id: tab.id, + url: url.startsWith('wexond-error') ? tab.errorURL : url, + title, + }); + } + + public hide() { + super.hide(); + this.webContents.send('visible', false); + } +} diff --git a/src/main/services/messaging.ts b/src/main/services/messaging.ts index ed11f5520..f77c9ae7c 100644 --- a/src/main/services/messaging.ts +++ b/src/main/services/messaging.ts @@ -47,6 +47,15 @@ export const runMessagingService = (appWindow: AppWindow) => { appWindow.searchDialog.toggle(); }); + ipcMain.on(`show-tab-preview-${id}`, (e, tab) => { + appWindow.previewDialog.tab = tab; + appWindow.previewDialog.show(); + }); + + ipcMain.on(`hide-tab-preview-${id}`, (e, tab) => { + appWindow.previewDialog.hide(); + }); + ipcMain.on(`update-find-info-${id}`, (e, tabId, data) => appWindow.findDialog.updateInfo(tabId, data), ); diff --git a/src/main/windows/app.ts b/src/main/windows/app.ts index 03678293f..0faeefa56 100644 --- a/src/main/windows/app.ts +++ b/src/main/windows/app.ts @@ -14,6 +14,7 @@ import { AuthDialog, FormFillDialog, CredentialsDialog, + PreviewDialog, } from '../dialogs'; export class AppWindow extends BrowserWindow { @@ -27,6 +28,7 @@ export class AppWindow extends BrowserWindow { public authDialog = new AuthDialog(this); public formFillDialog = new FormFillDialog(this); public credentialsDialog = new CredentialsDialog(this); + public previewDialog = new PreviewDialog(this); public incognito: boolean; @@ -136,6 +138,7 @@ export class AppWindow extends BrowserWindow { this.formFillDialog.destroy(); this.credentialsDialog.destroy(); this.permissionsDialog.destroy(); + this.previewDialog.destroy(); this.menuDialog = null; this.searchDialog = null; @@ -144,6 +147,7 @@ export class AppWindow extends BrowserWindow { this.formFillDialog = null; this.credentialsDialog = null; this.permissionsDialog = null; + this.previewDialog = null; this.viewManager.clear(); diff --git a/src/renderer/views/app/components/Toolbar/Tab/index.tsx b/src/renderer/views/app/components/Toolbar/Tab/index.tsx index 1480e5276..bbc9eccd1 100644 --- a/src/renderer/views/app/components/Toolbar/Tab/index.tsx +++ b/src/renderer/views/app/components/Toolbar/Tab/index.tsx @@ -8,7 +8,6 @@ import { StyledIcon, StyledTitle, StyledClose, - StyledBorder, StyledOverlay, TabContainer, } from './style'; @@ -47,10 +46,17 @@ const onMouseDown = (tab: ITab) => (e: React.MouseEvent) => { store.tabs.lastScrollLeft = store.tabs.containerRef.current.scrollLeft; }; -const onMouseEnter = (tab: ITab) => () => { +const onMouseEnter = (tab: ITab) => (e: React.MouseEvent) => { if (!store.tabs.isDragging) { store.tabs.hoveredTabId = tab.id; } + + const x = e.currentTarget.getBoundingClientRect().left; + + ipcRenderer.send(`show-tab-preview-${store.windowId}`, { + id: tab.id, + x, + }); }; const onMouseLeave = () => { @@ -232,7 +238,6 @@ export default observer(({ tab }: { tab: ITab }) => { onMouseLeave={onMouseLeave} visible={tab.tabGroupId === store.tabGroups.currentGroupId} ref={tab.ref} - title={tab.title} > store.tabs.containerRef.current; @@ -16,12 +17,13 @@ const onMouseEnter = () => { clearTimeout(timeout); }; -const onTabsMouseLeave = () => { +const onTabsMouseLeave = (e: React.MouseEvent) => { store.tabs.scrollbarVisible = false; timeout = setTimeout(() => { store.tabs.removedTabs = 0; store.tabs.updateTabsBounds(true); }, 300); + ipcRenderer.send(`hide-tab-preview-${store.windowId}`); }; const onAddTabClick = () => { diff --git a/src/renderer/views/preview/components/App/index.tsx b/src/renderer/views/preview/components/App/index.tsx new file mode 100644 index 000000000..ea43c5046 --- /dev/null +++ b/src/renderer/views/preview/components/App/index.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { observer } from 'mobx-react-lite'; +import { createGlobalStyle, ThemeProvider } from 'styled-components'; +import { hot } from 'react-hot-loader/root'; + +import { Style } from '../../style'; +import { StyledApp, Title, Domain } from './style'; +import store from '../../store'; + +const GlobalStyle = createGlobalStyle`${Style}`; + +export const App = hot( + observer(() => { + return ( + + + {store.title} + {store.domain} + + + + ); + }), +); diff --git a/src/renderer/views/preview/components/App/style.ts b/src/renderer/views/preview/components/App/style.ts new file mode 100644 index 000000000..727920027 --- /dev/null +++ b/src/renderer/views/preview/components/App/style.ts @@ -0,0 +1,45 @@ +import styled, { css } from 'styled-components'; +import { ITheme } from '~/interfaces'; +import { maxLines } from '~/renderer/mixins'; + +export const StyledApp = styled.div` + margin: 8px; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + border-radius: 6px; + overflow: hidden; + position: relative; + transition: 0.2s opacity, 0.2s margin-top; + padding: 16px; + font-size: 14px; + + ${({ visible, theme }: { visible: boolean; theme?: ITheme }) => css` + opacity: ${visible ? 1 : 0}; + margin-top: ${visible ? 0 : 7}px; + background-color: ${theme['dialog.backgroundColor']}; + color: ${theme['dialog.textColor']}; + `} +`; + +export const Title = styled.div` + font-weight: 500; + line-height: 1.3rem; + ${maxLines(2)}; +`; + +export const Domain = styled.div` + opacity: 0.54; + font-weight: 300; + line-height: 1.3rem; +`; + +export const Subtitle = styled.div` + font-size: 13px; + opacity: 0.54; + margin-top: 8px; +`; + +export const Buttons = styled.div` + display: flex; + margin-top: 16px; + float: right; +`; diff --git a/src/renderer/views/preview/index.tsx b/src/renderer/views/preview/index.tsx new file mode 100644 index 000000000..821e12727 --- /dev/null +++ b/src/renderer/views/preview/index.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { App } from './components/App'; +import { fonts } from '../../constants'; +import { ipcRenderer } from 'electron'; + +ipcRenderer.setMaxListeners(0); + +const styleElement = document.createElement('style'); + +styleElement.textContent = ` +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(${fonts.robotoRegular}) format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(${fonts.robotoMedium}) format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(${fonts.robotoLight}) format('woff2'); +} +`; + +document.head.appendChild(styleElement); + +ReactDOM.render(, document.getElementById('app')); diff --git a/src/renderer/views/preview/store/index.ts b/src/renderer/views/preview/store/index.ts new file mode 100644 index 000000000..9e5149e4c --- /dev/null +++ b/src/renderer/views/preview/store/index.ts @@ -0,0 +1,58 @@ +import { ipcRenderer, remote } from 'electron'; +import { observable, computed } from 'mobx'; +import { getTheme } from '~/utils/themes'; +import { ISettings } from '~/interfaces'; +import { DEFAULT_SETTINGS } from '~/constants'; +import { parse } from 'url'; + +export class Store { + @observable + public settings: ISettings = DEFAULT_SETTINGS; + + @computed + public get theme() { + return getTheme(this.settings.theme); + } + + @observable + public visible = false; + + @observable + public id = remote.getCurrentWebContents().id; + + @observable + public windowId = remote.getCurrentWindow().id; + + @observable + public title = ''; + + @observable + public url = ''; + + @computed + public get domain() { + return parse(this.url).hostname; + } + + public constructor() { + ipcRenderer.on('visible', (e, flag, tab) => { + this.visible = flag; + if (tab) { + this.title = tab.title; + this.url = tab.url; + } + }); + + ipcRenderer.send('get-settings'); + + ipcRenderer.on('update-settings', (e, settings: ISettings) => { + this.settings = { ...this.settings, ...settings }; + }); + } + + public hide() { + ipcRenderer.send(`hide-${this.id}`); + } +} + +export default new Store(); diff --git a/src/renderer/views/preview/style.ts b/src/renderer/views/preview/style.ts new file mode 100644 index 000000000..0eff013a6 --- /dev/null +++ b/src/renderer/views/preview/style.ts @@ -0,0 +1,20 @@ +import { css } from 'styled-components'; + +import { body2 } from '~/renderer/mixins'; + +export const Style = css` + body { + user-select: none; + cursor: default; + margin: 0; + padding: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + ${body2()} + } + + * { + box-sizing: border-box; + } +`; diff --git a/webpack.config.renderer.js b/webpack.config.renderer.js index a753cc685..31ed07121 100644 --- a/webpack.config.renderer.js +++ b/webpack.config.renderer.js @@ -26,6 +26,7 @@ applyEntries('app', appConfig, [ 'find', 'menu', 'search', + 'preview', ]); module.exports = appConfig;