diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9608d30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 09e5578..0000000 --- a/.eslintrc +++ /dev/null @@ -1,42 +0,0 @@ -{ - "extends": "google", - "plugins":[ - "html" - ], - "env": { - "browser": true - }, - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2017, - "ecmaFeatures": { - "modules": true - } - }, - "rules": { - "max-len": [2, 100, { - "ignoreComments": true, - "ignoreUrls": true, - "tabWidth": 2 - }], - "no-implicit-coercion": [2, { - "boolean": false, - "number": true, - "string": true - }], - "no-unused-expressions": [2, { - "allowShortCircuit": true, - "allowTernary": false - }], - "no-unused-vars": [2, { - "vars": "all", - "args": "after-used", - "argsIgnorePattern": "(^reject$|^_$)", - "varsIgnorePattern": "(^_$)" - }], - "quotes": [2, "single"], - "require-jsdoc": 0, - "valid-jsdoc": 0, - "arrow-parens": 0 - } -} diff --git a/.gitignore b/.gitignore index 3f808e8..7b21b03 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules dist npm-debug.log .vscode + +build/* +!build/assets diff --git a/README.md b/README.md index d316561..edbf256 100644 --- a/README.md +++ b/README.md @@ -6,42 +6,28 @@ # Clippy -> A clipboard manager built on [electron](https://github.com/electron/electron/), using [WebComponents](https://developer.mozilla.org/en-US/docs/Web/Web_Components) -## Installation -_Linux, and Windows 7+ are supported (64-bit only)_ +> A simple clipboard manager built on [electron](https://github.com/electron/electron/) -### Linux -[**Download**](https://github.com/ramlmn/electron-clippy/releases/latest) the `.AppImage` or `.deb` file -_The AppImage needs to be [made executable](http://discourse.appimage.org/t/how-to-make-an-appimage-executable/80) after download_ +## Screenshots +![Snippet view](media/screenshot-1.png) +![Image view](media/screenshot-2.png) +![Settings view](media/screenshot-3.png) -### Windows -[**Download**](https://github.com/ramlmn/electron-clippy/releases/latest) the `.exe` file -## Usage -By default the app would be running in the background and can be accessed by the keyboard shortcut: `Ctrl+Shift+V` +## Installation -## Dev -### Run -``` -$ npm install && npm start -``` +Prebuilt images can be found on the [releases](https://github.com/ramlmn/electron-clippy/releases/latest) page -### Build -``` -$ npm run build -``` -or check [`electron-builder` docs](https://www.electron.build/multi-platform-build) -## Acknowledgements -* UI inspired from [Alfred app](https://www.alfredapp.com/) -* Custom elements source inspired from [Polymer](https://github.com/Polymer/polymer) -* Installer icon inspired from icon made by [Roundicons](http://www.flaticon.com/authors/roundicons) from [www.flaticon.com](http://www.flaticon.com) is licensed by [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/) (_modified_) +## Usage/features + +- Persistent data storage across restarts (disabled by default) +- Delete individual items using the Delete key +- Clear whole clipboard at once +- Bring the app to forground the keyboard shortcut Ctrl/Command + Shift + V -## Screenshots -![Clippy screenshot snippet](media/clippy-snap1.png) -![Clippy screenshot image](media/clippy-snap2.png) ## License [MIT](LICENSE) diff --git a/src/build-assets/icon/png/128x128.png b/build/assets/icon/png/128x128.png similarity index 100% rename from src/build-assets/icon/png/128x128.png rename to build/assets/icon/png/128x128.png diff --git a/src/build-assets/icon/win/win.ico b/build/assets/icon/win/win.ico similarity index 100% rename from src/build-assets/icon/win/win.ico rename to build/assets/icon/win/win.ico diff --git a/media/clippy-snap1.png b/media/clippy-snap1.png deleted file mode 100644 index a972ec6..0000000 Binary files a/media/clippy-snap1.png and /dev/null differ diff --git a/media/clippy-snap2.png b/media/clippy-snap2.png deleted file mode 100644 index fc6ebf0..0000000 Binary files a/media/clippy-snap2.png and /dev/null differ diff --git a/media/screenshot-1.png b/media/screenshot-1.png new file mode 100644 index 0000000..33fefb3 Binary files /dev/null and b/media/screenshot-1.png differ diff --git a/media/screenshot-2.png b/media/screenshot-2.png new file mode 100644 index 0000000..f01beaf Binary files /dev/null and b/media/screenshot-2.png differ diff --git a/media/screenshot-3.png b/media/screenshot-3.png new file mode 100644 index 0000000..f928e41 Binary files /dev/null and b/media/screenshot-3.png differ diff --git a/package.json b/package.json index 6b556b0..ca5abcc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "clippy", - "version": "1.0.0-alpha.4", - "description": "A clipboard manager built on electron", + "name": "electron-clippy", + "version": "0.1.0", + "description": "A simple clipboard manager build on Electron", "license": "MIT", "repository": "ramlmn/electron-clippy", "author": { @@ -9,40 +9,62 @@ "email": "ramlmn@outlook.com", "url": "https://ramlmn.github.io/" }, - "homepage": "https://github.com/ramlmn/electron-clippy", - "main": "./src/main/main.js", + "homepage": "https://github.com/ramlmn/electron-clippy#readme", + "main": "build/main/main.js", "scripts": { - "lint": "eslint src", + "lint": "xo && stylelint src/**/*.css", + "lint-fix": "xo --fix; stylelint --fix src/**/*.css", "start": "electron .", - "build": "build" + "watch": "webpack --watch", + "dev": "concurrently \"npm run watch\" \"npm run start\"", + "build": "cross-env NODE_ENV=production webpack && build" + }, + "dependencies": { + "@ramlmn/view": "^1.0.0", + "auto-launch": "^5.0.5", + "global-dispatcher": "^1.0.0", + "hyperhtml": "^2.23.0", + "just-debounce-it": "^1.1.0" }, "devDependencies": { - "electron": "^1.8.4", - "electron-builder": "^20.6.2", - "eslint": "^4.19.1", - "eslint-config-google": "^0.9.1", - "eslint-plugin-html": "^4.0.2" + "clean-webpack-plugin": "^1.0.0", + "concurrently": "^4.1.0", + "copy-webpack-plugin": "^4.6.0", + "cross-env": "^5.2.0", + "css-loader": "^2.0.2", + "cssnano": "^4.1.8", + "electron": "^4.0.0", + "electron-builder": "^20.38.4", + "file-loader": "^3.0.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "stylelint": "^9.9.0", + "stylelint-config-xo-space": "^0.11.0", + "webpack": "^4.28.2", + "webpack-cli": "^3.1.2", + "xo": "^0.23.0" }, "build": { + "appId": "com.ramlmn.electron-clippy", "productName": "Clippy", - "appId": "com.ramlmn.clippy", + "artifactName": "electron-clippy-${version}.${ext}", "files": [ - "src/**/*", - "node_modules", - "!(*.markdown|*.md|*.txt|.*|LICENSE|README|test)" + "build/**/*", + "node_modules" ], "linux": { "category": "Productivity", - "icon": "src/build-assets/icon/png", + "icon": "build/assets/icon/png", "target": [ "AppImage", "deb" ] }, "win": { - "icon": "src/build-assets/icon/win/win.ico", + "icon": "build/assets/icon/win/win.ico", "target": [ - "nsis" + "nsis", + "msi" ] }, "nsis": { @@ -52,7 +74,38 @@ "deleteAppDataOnUninstall": true } }, - "dependencies": { - "auto-launch": "^5.0.5" + "xo": { + "space": true, + "rules": { + "no-unused-expressions": [ + 2, + { + "allowTaggedTemplates": true + } + ], + "import/no-unassigned-import": [ + 1, + { + "allow": [ + "**/*.css" + ] + } + ] + }, + "env": [ + "node", + "browser" + ], + "ignore": [ + "app" + ] + }, + "stylelint": { + "extends": "stylelint-config-xo-space", + "rules": { + "declaration-colon-newline-after": null, + "selector-list-comma-newline-after": null, + "selector-type-no-unknown": null + } } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..2c18b11 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,24 @@ +export const EVENT = { + APP_CLOSE: 'app-close', + APP_HIDE: 'app-hide', + APP_INIT: 'app-init', + APP_SHOW: 'app-show', + APP_STATS: 'app-stats', + + ITEM_DELETE: 'item-delete', + ITEM_NEW: 'item-new', + ITEM_NEXT: 'item-next', + ITEM_PREVIOUS: 'item-previous', + ITEM_RENDER: 'item-render', + ITEM_SEARCH: 'item-search', + ITEM_COPY: 'item-copy', + + ITEMS_SAVE: 'items-save', + ITEMS_RESTORE: 'items-restore', + ITEMS_CLEAR: 'items-clear', + + SETTINGS_UPDATE: 'settings-update', // main -> renderer + SETTINGS_CHANGE: 'settings-change', // renderer -> main + SETTINGS_HIDE: 'settings-hide', + SETTINGS_SHOW: 'settings-show' +}; diff --git a/src/main/clipboard-watcher.js b/src/main/clipboard-watcher.js index a9f8f18..512eb6b 100644 --- a/src/main/clipboard-watcher.js +++ b/src/main/clipboard-watcher.js @@ -1,9 +1,6 @@ -'use strict'; - -const {clipboard} = require('electron'); -const EventEmitter = require('events'); -const crypto = require('crypto'); - +import {clipboard} from 'electron'; +import EventEmitter from 'events'; +import crypto from 'crypto'; /** * A thing which tracks the system clipboard and calls a callback, @@ -20,12 +17,10 @@ class ClipboardWatcher extends EventEmitter { // An object storing the recent clipboard item this._recentClipItem = {}; - // this.clipboardItems = new Map(); this._watchLoop = this._watchLoop.bind(this); } - /** * Start listening for new items in clipboard * @@ -39,7 +34,6 @@ class ClipboardWatcher extends EventEmitter { this._watchLoop(); } - /** * A recursive function which listens for changes in system clipboard * @@ -56,7 +50,6 @@ class ClipboardWatcher extends EventEmitter { setTimeout(this._watchLoop, 1000); } - /** * The function that scrapes the clipboard, analyzes all the available types * and generates a clipboard item @@ -81,7 +74,7 @@ class ClipboardWatcher extends EventEmitter { text: '', html: '', rtf: '', - image: '', + image: '' }, // Data for image @@ -90,12 +83,12 @@ class ClipboardWatcher extends EventEmitter { height: 0, // Data for text - length: 0, + length: 0 }; // Extract all the available formats of data on the clipboard for (const format of availableFormats) { - // html and rtf formats are also considered plain text + // HTML and RTF formats are also considered plain text if (format.startsWith('text/')) { newClipItem.type = 'text'; @@ -126,7 +119,7 @@ class ClipboardWatcher extends EventEmitter { // also cryptographic hash to identify them if (newClipItem.type === 'image') { newClipItem.hash = crypto - .createHash('sha256') + .createHash('md5') .update(newClipItem.data.image) .digest('hex'); @@ -141,7 +134,7 @@ class ClipboardWatcher extends EventEmitter { newClipItem.length = [...newClipItem.data.text].length; newClipItem.hash = crypto - .createHash('sha256') + .createHash('md5') .update(newClipItem.data.text) .digest('hex'); } else { @@ -154,7 +147,6 @@ class ClipboardWatcher extends EventEmitter { this.emit('item', newClipItem); } - /** * Generates a base64 thumbnail for the provided native image * @@ -168,7 +160,7 @@ class ClipboardWatcher extends EventEmitter { const resizeOptions = { width: 300, - height: 300, + height: 300 }; if (imageDimensions.width > resizeOptions.width) { @@ -187,13 +179,12 @@ class ClipboardWatcher extends EventEmitter { return thumb.toDataURL(); } - /** * Compares with the old item available and determines if it is a new item * or exactly same as the old one * - * @param {Object} newItem - * @returns {Boolean} + * @param {Object} newItem The possibly new item object to check for + * @returns {Boolean} Returns true if same otherwise false * @memberof ClipboardWatcher */ _isNewItem(newItem) { @@ -214,7 +205,9 @@ class ClipboardWatcher extends EventEmitter { // Text is considered new if any of `text`, `html`, `rtf` parts change if (oldItem.type === 'image') { return (oldItem.data.image !== newItem.data.image); - } else if (oldItem.type === 'text') { + } + + if (oldItem.type === 'text') { return ( oldItem.data.text !== newItem.data.text || oldItem.data.html !== newItem.data.html || @@ -224,5 +217,4 @@ class ClipboardWatcher extends EventEmitter { } } - -exports.ClipboardWatcher = ClipboardWatcher; +export default ClipboardWatcher; diff --git a/src/main/main.js b/src/main/main.js index 8aa9723..fac33e9 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -1,77 +1,85 @@ -'use strict'; - -const url = require('url'); -const path = require('path'); -const autoLaunch = new (require('auto-launch'))({name: 'Clippy'}); -const {app, Menu, BrowserWindow, ipcMain, globalShortcut, Tray} = require('electron'); -const {ClipboardWatcher} = require('./clipboard-watcher.js'); +import fs from 'fs'; +import url from 'url'; +import path from 'path'; +import {promisify} from 'util'; +import {app, Menu, BrowserWindow, ipcMain, globalShortcut, Tray} from 'electron'; +import AutoLaunch from 'auto-launch'; +import ClipboardWatcher from './clipboard-watcher'; +import {EVENT} from '../constants'; + +const stat = promisify(fs.stat); +const unlink = promisify(fs.unlink); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const isProduction = process.env.NODE_ENV === 'production'; +const autoLaunch = new AutoLaunch({name: 'Clippy'}); let mainWindow = null; let rendererChannel = null; let tray = null; let accStat = null; -let startupStat = null; + +const userDataPath = app.getPath('userData'); +const settingsFilePath = path.resolve(userDataPath, 'settings.json'); +const historyFilePath = path.resolve(userDataPath, 'history.json'); +const indexFilePath = path.resolve(__dirname, '../renderer/index.html'); +const trayIconPath = path.resolve(__dirname, '../renderer/img/clippy-32.png'); + +// default app settings, will be updated later +const appSettings = { + runOnStartup: false, + persistentHistory: false +}; + +const browserWindowOptions = { + width: 800, + height: 500, + show: false, + center: true, + resizable: false, + minimizable: false, + maximizable: !isProduction, + closable: !isProduction, + fullscreenable: false, + skipTaskbar: true, + movable: false, + frame: false, + transparent: true, + title: 'Clippy', + alwaysOnTop: true +}; const trayTemplate = [{ - label: 'Clippy', + label: 'Toggle Dev Tools', + click: () => rendererChannel && rendererChannel.toggleDevTools() }, { - type: 'separator', + type: 'separator' }, { - label: 'Show', - click: showWindow, + label: 'Show Clippy', + click: showWindow }, { label: 'Clear', - click: _ => { - rendererChannel.send('clear-items'); - }, + click: () => { + rendererChannel.send(EVENT.ITEMS_CLEAR); + } }, { label: 'Quit', - click: _ => { + click: () => { mainWindow.close(); - }, - }]; - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -app.on('activate', () => { - if (!mainWindow) { - mainWindow = createMainWindow(); - rendererChannel = mainWindow.webContents; + } } -}); - -app.on('ready', () => { - mainWindow = createMainWindow(); - accStat = addEventListeners(); - rendererChannel = mainWindow.webContents; -}); - - -// Settingup clipboard watcher -const watcher = new ClipboardWatcher(); -watcher.on('item', data => { - rendererChannel.send('clipboard-item', data); -}); - - -ipcMain.once('init', onInit); - -ipcMain.on('hide', hideWindow); - -ipcMain.on('settings', handleSettings); +]; +function registerGlobalShortcut() { + return globalShortcut.register('CommandOrControl+Shift+V', showWindow); +} -function onClosed() { - // Dereference the window +function onWindowClosed() { mainWindow = null; } function showWindow(event) { - // Show the window mainWindow.show(); if (event) { @@ -89,73 +97,147 @@ function hideWindow(event) { } function createMainWindow() { - const win = new BrowserWindow({ - width: 700, - height: 450, - show: false, - center: true, - resizable: false, - fullscreenable: false, - skipTaskbar: true, - movable: false, - frame: false, - transparent: true, - title: 'Clippy', - alwaysOnTop: true, - }); + const win = new BrowserWindow(browserWindowOptions); // Basic events for window - win.on('closed', onClosed); + win.on('closed', onWindowClosed); win.on('minimize', hideWindow); win.on('blur', hideWindow); const urlToLoad = url.format({ - pathname: path.join(__dirname, '../renderer/index.html'), + pathname: indexFilePath, protocol: 'file:', - slashes: true, + slashes: true }); win.loadURL(urlToLoad); + tray = new Tray(trayIconPath); - // Settingup tray icon - tray = new Tray(path.join(__dirname, '../renderer/img/clip-32x32.png')); - - const trayContetxtMenu = Menu.buildFromTemplate(trayTemplate); - tray.setContextMenu(trayContetxtMenu); + const trayContextMenu = Menu.buildFromTemplate(trayTemplate); + tray.setContextMenu(trayContextMenu); tray.setToolTip('Clippy'); tray.setTitle('Clippy'); tray.on('double-click', showWindow); + (async () => { + try { + // merge default and saved settings + const data = await readFile(settingsFilePath, {encoding: 'utf-8'}); + Object.assign(appSettings, JSON.parse(data)); + } catch (err) { + console.error('[ERR] Error reading settings', settingsFilePath); + console.error(err); + } + })(); + return win; } -function addEventListeners() { - return globalShortcut.register('CommandOrControl+Shift+V', showWindow); +async function persistItems(items) { + try { + await writeFile(historyFilePath, JSON.stringify(items, null, isProduction ? '' : ' ')); + console.log('[INF] Wrote history to', historyFilePath); + } catch (error) { + console.error('[ERR] Error while saving history:', historyFilePath); + console.error(error); + } } -async function onInit() { - // Start watching clipboard - watcher.startListening(); - - // Check startup status - startupStat = await autoLaunch.isEnabled(); - - // Send stats to renderer - rendererChannel.send('stats', {accelerator: accStat, startup: startupStat}); +async function persistSettings() { + try { + await writeFile(settingsFilePath, JSON.stringify(appSettings, null, ' ')); + console.log('[INF] Wrote settings to', settingsFilePath); + } catch (error) { + console.error('[ERR] Error while writing settings file:', settingsFilePath); + console.error(error); + } } -async function handleSettings(event, args) { - if (args.startup !== startupStat) { - // Setting changed - if (args.startup === true) { +async function onSettingsChange(event, settings) { + if (settings) { + if (settings.runOnStartup === true) { autoLaunch.enable(); } else { autoLaunch.disable(); } - startupStat = await autoLaunch.isEnabled(); - rendererChannel.send('stats', {startup: startupStat}); + if (settings.persistentHistory === true) { + // start saving items, flush current to drive + } else { + // stop saving items, delete everything from drive + try { + const fileStat = await stat(historyFilePath); + if (fileStat.isFile()) { + await unlink(historyFilePath); + } + } catch (error) {} + + } + + appSettings.persistentHistory = settings.persistentHistory; } + + appSettings.runOnStartup = await autoLaunch.isEnabled(); + + persistSettings(); // flush settings immediately + + rendererChannel.send(EVENT.SETTINGS_UPDATE, appSettings); } + +async function onAppInit() { + const clipboardWatcher = new ClipboardWatcher(); + clipboardWatcher.on('item', (item) => { + rendererChannel.send(EVENT.ITEM_NEW, item); + }); + clipboardWatcher.startListening(); + + if (appSettings.persistentHistory === true) { + try { + console.log('[INF] Restoring persistent history'); + const data = await readFile(historyFilePath, {encoding: 'utf-8'}); + rendererChannel.send(EVENT.ITEMS_RESTORE, JSON.parse(data)); + } catch (error) { + console.error('[ERR] Failed to restore persistent history'); + } + } + + // send settings to renderer at startup + await onSettingsChange(); +} + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (!mainWindow) { + mainWindow = createMainWindow(); + rendererChannel = mainWindow.webContents; + } +}); + +app.on('ready', () => { + try { + mainWindow = createMainWindow(); + accStat = registerGlobalShortcut(); + rendererChannel = mainWindow.webContents; + } catch (error) { + console.error(error); + process.exit(1); + } +}); + +ipcMain.on(EVENT.APP_INIT, onAppInit); +ipcMain.on(EVENT.APP_HIDE, hideWindow); + +ipcMain.on(EVENT.SETTINGS_CHANGE, onSettingsChange); + +ipcMain.on(EVENT.ITEMS_SAVE, (event, items) => { + if (appSettings.persistentHistory) { + persistItems(items); + } +}); diff --git a/src/renderer/components/clippy-app.html b/src/renderer/components/clippy-app.html deleted file mode 100644 index 0b0551c..0000000 --- a/src/renderer/components/clippy-app.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/src/renderer/components/clippy-app/clippy-app.css b/src/renderer/components/clippy-app/clippy-app.css new file mode 100644 index 0000000..ec23f93 --- /dev/null +++ b/src/renderer/components/clippy-app/clippy-app.css @@ -0,0 +1,43 @@ +clippy-app { + display: flex; + position: relative; + flex-direction: column; + flex: 1; + background: #fff; + overflow: hidden; + margin: 1rem; + border-radius: 0.3em; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.14), + 0 4px 18px 0 rgba(0, 0, 0, 0.12), + 0 3px 5px -2px rgba(0, 0, 0, 0.2); +} + +clippy-app.is-linux { + margin: 0; + box-shadow: none; + border-radius: none; +} + +clippy-app .clippy-toolbar { + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + grid-gap: 0.5rem; + padding: 0.5rem; +} + +clippy-app .clippy-toolbar clippy-search { + flex: 1; +} + +clippy-app .clippy-content { + display: grid; + grid-template: 1fr / repeat(2, calc(50% - 0.25rem)); + grid-gap: 0.5rem; + flex: 1; + padding: 0.5rem; +} + +clippy-app .clippy-content > * { + min-height: 0; +} diff --git a/src/renderer/components/clippy-app/index.js b/src/renderer/components/clippy-app/index.js new file mode 100644 index 0000000..a2c7b4a --- /dev/null +++ b/src/renderer/components/clippy-app/index.js @@ -0,0 +1,76 @@ +import {dispatch} from 'global-dispatcher'; +import {viewIn, shouldHandle} from '@ramlmn/view'; +import ClippyElement from '../clippy-element'; +import {EVENT} from '../../../constants'; +import '../clippy-settings'; +import '../clippy-search'; +import '../clippy-button'; +import '../clippy-items'; +import '../clippy-previewer'; +import './clippy-app.css'; + +class ClippyApp extends ClippyElement { + constructor() { + super(); + + if (process.platform === 'linux') { + this.classList.add('is-linux'); + } + } + + set view(v) { + this._view = v; + return this._view; + } + + get view() { + return this._view; + } + + connectedCallback() { + this._view = viewIn(); + + document.addEventListener('keydown', event => { + if (!shouldHandle(this.view)) { + return; + } + + if (event.code === 'ArrowDown') { + dispatch(EVENT.ITEM_NEXT); + event.preventDefault(); + } else if (event.code === 'ArrowUp') { + dispatch(EVENT.ITEM_PREVIOUS); + event.preventDefault(); + } else if (event.code === 'Delete') { + dispatch(EVENT.ITEM_DELETE); + event.preventDefault(); + } else if (event.code === 'Enter') { + dispatch(EVENT.ITEM_COPY); + } else if (event.code === 'Escape') { + dispatch(EVENT.APP_HIDE); + } + }); + + this.render(); + } + + render() { + this.html` + +
+ + +
+
+ + +
+ `; + } +} + +customElements.define('clippy-app', ClippyApp); + +export default ClippyApp; diff --git a/src/renderer/components/clippy-button/clippy-button.css b/src/renderer/components/clippy-button/clippy-button.css new file mode 100644 index 0000000..214ef5a --- /dev/null +++ b/src/renderer/components/clippy-button/clippy-button.css @@ -0,0 +1,28 @@ +clippy-button { + width: 2.25rem; + height: 2.25rem; + padding: 0.125rem; + display: grid; + align-items: center; + justify-content: center; + position: relative; +} + +clippy-button:focus, +clippy-button:active { + outline: none; +} + +clippy-button svg { + padding: 0.125rem; + height: 100%; + fill: var(--color-accented-700, currentColor); + opacity: 0.8; + transition: opacity 0.2s ease; +} + +clippy-button:hover svg, +clippy-button:focus svg, +clippy-button:active svg { + opacity: 1; +} diff --git a/src/renderer/components/clippy-button/index.js b/src/renderer/components/clippy-button/index.js new file mode 100644 index 0000000..6212aa4 --- /dev/null +++ b/src/renderer/components/clippy-button/index.js @@ -0,0 +1,54 @@ +import ClippyElement from '../clippy-element'; +import './clippy-button.css'; + +class ClippyButton extends ClippyElement { + connectedCallback() { + this.role = 'button'; + this.tabIndex = 0; + + this._icon = this.getAttribute('icon'); + this._label = this.getAttribute('label'); + + this.setAttribute('aria-label', this._label); + + this.addEventListener('mousedown', () => this._pressed()); + this.addEventListener('mouseup', () => this._released()); + + this.addEventListener('keydown', event => { + if (event.code === 'Space' || event.code === 'Enter') { + this._pressed(); + } + }); + + this.addEventListener('keyup', event => { + if (event.code === 'Space' || event.code === 'Enter') { + this._released(); + this.dispatchEvent(new CustomEvent('click', { + bubbles: false + })); + } + }); + + this.render(); + } + + _pressed() { + this.setAttribute('aria-pressed', true); + } + + _released() { + this.setAttribute('aria-pressed', false); + } + + render() { + this.html` + + + + `; + } +} + +customElements.define('clippy-button', ClippyButton); + +export default ClippyButton; diff --git a/src/renderer/components/clippy-element.html b/src/renderer/components/clippy-element.html deleted file mode 100644 index a89fcf9..0000000 --- a/src/renderer/components/clippy-element.html +++ /dev/null @@ -1,98 +0,0 @@ - diff --git a/src/renderer/components/clippy-element/index.js b/src/renderer/components/clippy-element/index.js new file mode 100644 index 0000000..ba48c64 --- /dev/null +++ b/src/renderer/components/clippy-element/index.js @@ -0,0 +1,33 @@ +import {bind, wire} from 'hyperhtml/esm'; + +class ClippyElement extends HTMLElement { + constructor(...args) { + super(...args); + this.html = bind(this); + this.wire = wire(this); + this.state = {}; + } + + connectedCallback() { + this.render(); + } + + render() {} + + setState(state, render) { + const target = this.state; + const source = typeof state === 'function' ? state.call(this, target) : state; + + for (const key in source) { + target[key] = source[key]; + } + + if (render !== false) { + this.render(); + } + + return this; + } +} + +export default ClippyElement; diff --git a/src/renderer/components/clippy-item/clippy-item.css b/src/renderer/components/clippy-item/clippy-item.css new file mode 100644 index 0000000..16e80be --- /dev/null +++ b/src/renderer/components/clippy-item/clippy-item.css @@ -0,0 +1,13 @@ +clippy-item { + display: block; + padding: 0.25rem 0.75rem; + border-radius: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +clippy-item.selected { + background-color: var(--color-item-active-bg, #212121); + color: var(--color-item-active-fg, #fff); +} diff --git a/src/renderer/components/clippy-item/index.js b/src/renderer/components/clippy-item/index.js new file mode 100644 index 0000000..8fabefb --- /dev/null +++ b/src/renderer/components/clippy-item/index.js @@ -0,0 +1,12 @@ +import ClippyElement from '../clippy-element'; +import './clippy-item.css'; + +class ClippyItem extends ClippyElement { + connectedCallback() { + this.setAttribute('role', 'listitem'); + } +} + +customElements.define('clippy-item', ClippyItem); + +export default ClippyItem; diff --git a/src/renderer/components/clippy-items.html b/src/renderer/components/clippy-items.html deleted file mode 100644 index 3a288cc..0000000 --- a/src/renderer/components/clippy-items.html +++ /dev/null @@ -1,357 +0,0 @@ - - - - - diff --git a/src/renderer/components/clippy-items/clippy-items.css b/src/renderer/components/clippy-items/clippy-items.css new file mode 100644 index 0000000..adddcc9 --- /dev/null +++ b/src/renderer/components/clippy-items/clippy-items.css @@ -0,0 +1,38 @@ +clippy-items { + overflow-y: auto; + padding-right: 0.5rem; + position: relative; +} + +clippy-items clippy-item { + display: block; + padding: 0.25rem 0.75rem; + border-radius: 2px; + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +clippy-items clippy-item.selected { + background-color: var(--color-item-active-bg, #212121); + color: var(--color-item-active-fg, #fff); +} + +clippy-items .not-found { + color: var(--color-primary-500, #ccc); + user-select: none; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +clippy-items .not-found p { + margin: 0.25rem 0; +} diff --git a/src/renderer/components/clippy-items/index.js b/src/renderer/components/clippy-items/index.js new file mode 100644 index 0000000..03f43a9 --- /dev/null +++ b/src/renderer/components/clippy-items/index.js @@ -0,0 +1,228 @@ +import {clipboard, nativeImage} from 'electron'; +import {subscribe, dispatch} from 'global-dispatcher'; +import {wire} from 'hyperhtml/esm'; +import debounce from 'just-debounce-it'; +import ClippyElement from '../clippy-element'; +import {clamp} from '../../util'; +import {EVENT} from '../../../constants'; +import '../clippy-item'; +import './clippy-items.css'; + +class ClippyItems extends ClippyElement { + constructor() { + super(); + + this.items = new Map(); + this._items = []; + this._selectedItem = null; + this._itemsToRender = []; + this.pattern = null; + + this._saveItemsLazily = debounce(this._saveItems, 200); + } + + connectedCallback() { + this.role = 'list'; + + subscribe(EVENT.ITEM_NEW, item => this.handleNewItem(item)); + subscribe(EVENT.ITEM_DELETE, hash => this.handleDeleteItem(hash)); + subscribe(EVENT.ITEM_SEARCH, pattern => this.handleSearch(pattern)); + + subscribe(EVENT.ITEM_NEXT, () => this._selectNext()); + subscribe(EVENT.ITEM_COPY, () => this._copyItem()); + subscribe(EVENT.ITEM_PREVIOUS, () => this._selectPrevious()); + + subscribe(EVENT.ITEMS_CLEAR, () => this.handleClearItems()); + subscribe(EVENT.ITEMS_RESTORE, (items) => this.handleRestoreItems(items)); + + this.render(); + } + + _saveItems() { + dispatch(EVENT.ITEMS_SAVE, this._items); + } + + handleClearItems() { + this.items.clear(); + this._items = []; + this._selectedItem = null; + this._itemsToRender = []; + + clipboard.clear(); + + this._saveItems(); + + this.render(); + } + + handleRestoreItems(items) { + this.items.clear(); + this._items = []; + + for (const item of items) { + this.items.set(item.hash, item); + this._items.push(item); + this._items = this._sortItems(this._items); + } + + this._itemsToRender = this._items; + this._selectedItem = this._items[0]; + + this.render(); + } + + handleNewItem(item) { + if (this.items.has(item.hash)) { + this._items = this._items.filter(i => i.hash !== item.hash); + } + + this.items.set(item.hash, item); + this._items.unshift(item); + this._items = this._sortItems(this._items); + + if (!this._pattern) { + this._selectedItem = item; + this._itemsToRender = this._items; + } + + this._saveItemsLazily(); + + this.render(); + } + + handleDeleteItem(hash = this._selectedItem.hash) { + const position = this._itemsToRender.indexOf(this._selectedItem); + const length = this._itemsToRender.length; + + this.items.delete(hash); + this._items = this._items.filter(item => item.hash !== hash); + this._itemsToRender = this._itemsToRender.filter(item => item.hash !== hash); + + if (position === length - 1) { + this._selectPrevious(); + } else { + this._selectNext(); + } + + this._saveItemsLazily(); + + this.render(); + } + + _sortItems(items) { + return items.sort((a, b) => { + return b.timestamp - a.timestamp; + }); + } + + _select(offset) { + if (this._selectedItem) { + const currentIndex = this._itemsToRender.indexOf(this._selectedItem); + const nextIndex = clamp(currentIndex + offset, 0, this._itemsToRender.length - 1); + this._selectedItem = this._itemsToRender[nextIndex]; + + if (currentIndex === nextIndex) { + return; + } + } else { + this._selectedItem = this._itemsToRender[0]; + } + + this.render(); + } + + _selectPrevious() { + this._select(-1); + } + + _selectNext() { + this._select(1); + } + + _copyItem() { + const selected = this._selectedItem; + + if (selected) { + if (selected.type === 'image') { + clipboard.write({ + image: nativeImage.createFromDataURL(selected.data.image), + ...selected.data + }); + } else { + clipboard.write({ + ...selected.data + }); + } + } + } + + _filterItems(pattern) { + if (pattern) { + // @TODO: Maybe use a better search algorithm + try { + const re = new RegExp(pattern, 'gim'); + const items = this._items.filter(item => { + return item.type === 'text' && item.data.text.match(re); + }); + + this._selectedItem = items[0]; + + return items; + } catch (e) { + this._selectedItem = this._items[0]; + } + } + + if (!this._selectedItem) { + this._selectedItem = this._items[0]; + } + + return this._items; + } + + handleSearch(pattern) { + this._pattern = pattern; + this._itemsToRender = this._filterItems(this._pattern); + this.render(); + } + + render() { + dispatch(EVENT.ITEM_RENDER, this._selectedItem); + + if (this._itemsToRender.length > 0) { + this.html` + ${this._itemsToRender.map(item => wire(this, `:clippy-item-${item.hash}`)` + + ${item.type === 'image' + ? `Image: ${item.width}x${item.height}` + : item.data.text.trim().substr(0, 100)} + + `)} + `; + + requestAnimationFrame(() => { + const currentItem = this.querySelector('clippy-item.selected'); + if (currentItem) { + currentItem.scrollIntoView({ + block: 'nearest' + }); + } + }); + } else { + this.html` +
+

¯\\_(ツ)_/¯

+

We couldn't find anything

+
+ `; + } + } +} + +customElements.define('clippy-items', ClippyItems); + +export default ClippyItems; diff --git a/src/renderer/components/clippy-previewer.html b/src/renderer/components/clippy-previewer.html deleted file mode 100644 index 5df6fc4..0000000 --- a/src/renderer/components/clippy-previewer.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/src/renderer/components/clippy-previewer/clippy-previewer.css b/src/renderer/components/clippy-previewer/clippy-previewer.css new file mode 100644 index 0000000..3de0671 --- /dev/null +++ b/src/renderer/components/clippy-previewer/clippy-previewer.css @@ -0,0 +1,46 @@ +clippy-previewer { + display: flex; + flex-direction: column; +} + +clippy-previewer .preview { + background: var(--color-primary-50, #fafafa); + display: flex; + flex: 1; +} + +clippy-previewer .preview .preview-text, +clippy-previewer .preview .preview-image { + width: 100%; + height: 100%; + padding: 0.5rem; +} + +clippy-previewer .preview .preview-text { + overflow: auto; + white-space: pre; + font-family: Consolas, SFMono-Regular, Menlo, Monaco, 'Droid Sans Mono', 'Source Code Pro', + 'DejaVu Sans Mono', 'Ubuntu Mono', Courier, monospace; + line-height: 1.25; + color: var(--color-primary-800); +} + +clippy-previewer .preview .preview-image { + object-fit: scale-down; + background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%202%202%22%3E%3Cpath%20d%3D%22M1%202V0h1v1H0v1z%22%20fill-opacity%3D%22.05%22%2F%3E%3C%2Fsvg%3E'); + background-size: 1rem; +} + +clippy-previewer .preview-meta { + font-size: 0.85rem; + margin-top: 0.5rem; + user-select: none; +} + +clippy-previewer .preview-meta p { + margin: 0; + min-height: 1.25rem; + line-height: 1.25rem; + text-align: center; + color: var(--color-primary-600); +} diff --git a/src/renderer/components/clippy-previewer/index.js b/src/renderer/components/clippy-previewer/index.js new file mode 100644 index 0000000..82ba5d4 --- /dev/null +++ b/src/renderer/components/clippy-previewer/index.js @@ -0,0 +1,66 @@ +import {wire} from 'hyperhtml/esm'; +import {subscribe} from 'global-dispatcher'; +import ClippyElement from '../clippy-element'; +import {EVENT} from '../../../constants'; +import './clippy-previewer.css'; + +class ClippyPreviewer extends ClippyElement { + constructor() { + super(); + + subscribe(EVENT.ITEM_RENDER, item => this.render(item)); + } + + connectedCallback() { + this.render(); + } + + getPreviewTextOrImage(item) { + if (item) { + if (item.type === 'image') { + return wire()``; + } else { + return wire()`
${item.data.text}
`; + } + } + + return wire()`
`; + } + + getPreviewMeta(item) { + if (item) { + let meta; + + if (item.type === 'image') { + meta = wire()`${item.width} x ${item.height}`; + } else { + meta = wire()`${[...item.data.text].length} chars`; + } + + return wire()` +

Copied at ${(new Date(item.timestamp)).toLocaleString()}

+

${meta}

+ `; + } else { + return wire()` +

+

+ `; + } + } + + render(item) { + this.html` +
+ ${this.getPreviewTextOrImage(item)} +
+
+ ${this.getPreviewMeta(item)} +
+ `; + } +} + +customElements.define('clippy-previewer', ClippyPreviewer); + +export default ClippyElement; diff --git a/src/renderer/components/clippy-search/clippy-search.css b/src/renderer/components/clippy-search/clippy-search.css new file mode 100644 index 0000000..b36a1de --- /dev/null +++ b/src/renderer/components/clippy-search/clippy-search.css @@ -0,0 +1,28 @@ +clippy-search { + display: flex; +} + +clippy-search input[type='search'] { + flex: 1; + padding: 0.5rem; + border: none; + border-radius: 3px; + color: inherit; + font-family: inherit; + font-size: 1.25rem; + background-color: var(--color-primary-50); +} + +clippy-search input[type='search']:focus, +clippy-search input[type='search']:hover, +clippy-search input[type='search']:active { + outline: none; +} + +clippy-search input[type='search']::-webkit-search-decoration, +clippy-search input[type='search']::-webkit-search-cancel-button, +clippy-search input[type='search']::-webkit-search-results-button, +clippy-search input[type='search']::-webkit-search-results-decoration { + display: none; +} + diff --git a/src/renderer/components/clippy-search/index.js b/src/renderer/components/clippy-search/index.js new file mode 100644 index 0000000..3293a92 --- /dev/null +++ b/src/renderer/components/clippy-search/index.js @@ -0,0 +1,64 @@ +import {dispatch} from 'global-dispatcher'; +import {shouldHandle} from '@ramlmn/view'; +import ClippyElement from '../clippy-element'; +import {EVENT} from '../../../constants'; +import './clippy-search.css'; + +class ClippySearch extends ClippyElement { + constructor() { + super(); + + this._value = ''; + } + + connectedCallback() { + this._autoFocus = this.getAttribute('autofocus'); + + document.addEventListener('keyup', event => { + if (!shouldHandle(this.view)) { + return; + } + + const input = this.querySelector('input'); + if (document.activeElement !== input && !event.defaultPrevented) { + input.focus(); + } + }); + + this.render(); + } + + get view() { + return this.dataset.view; + } + + onChange(event) { + this._value = event.target.value; + dispatch(EVENT.ITEM_SEARCH, this._value); + } + + static get observedAttributes() { + return ['autofocus']; + } + + attributeChangedCallback(name, oldVal, newVal) { + if (name === 'autofocus') { + this._autoFocus = newVal; + this.render(); + } + } + + render() { + this.html` + + `; + } +} + +customElements.define('clippy-search', ClippySearch); + +export default ClippySearch; diff --git a/src/renderer/components/clippy-settings.html b/src/renderer/components/clippy-settings.html deleted file mode 100644 index 3ff17f8..0000000 --- a/src/renderer/components/clippy-settings.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - diff --git a/src/renderer/components/clippy-settings/clippy-settings.css b/src/renderer/components/clippy-settings/clippy-settings.css new file mode 100644 index 0000000..347856e --- /dev/null +++ b/src/renderer/components/clippy-settings/clippy-settings.css @@ -0,0 +1,57 @@ +clippy-settings { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 9999; + pointer-events: none; + user-select: none; +} + +clippy-settings.shown { + pointer-events: initial; +} + +clippy-settings .scrim { + position: absolute; + left: 0; + right: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +clippy-settings.shown .scrim { + opacity: 1; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +clippy-settings .content { + max-width: 100%; + width: 32rem; + position: absolute; + top: 0; + left: 0; + right: 0; + margin: auto; + padding: 1rem; + background: #fff; + transform: translateY(-110%); + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + transition: transform 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +clippy-settings.shown .content { + display: grid; + grid-gap: 1rem; + transform: translateY(0); + transition: transform 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +clippy-settings .content h2 { + margin: 0; +} diff --git a/src/renderer/components/clippy-settings/index.js b/src/renderer/components/clippy-settings/index.js new file mode 100644 index 0000000..c073052 --- /dev/null +++ b/src/renderer/components/clippy-settings/index.js @@ -0,0 +1,124 @@ +import {subscribe, dispatch} from 'global-dispatcher'; +import {viewIn, viewOut, shouldHandle} from '@ramlmn/view'; +import ClippyElement from '../clippy-element'; +import {EVENT} from '../../../constants'; +import '../clippy-switch'; +import './clippy-settings.css'; + +class ClippySettings extends ClippyElement { + constructor() { + super(); + + this._settings = { + runOnStartup: false, + persistentHistory: false + }; + + subscribe(EVENT.SETTINGS_SHOW, () => this.show()); + subscribe(EVENT.SETTINGS_HIDE, () => this.hide()); + subscribe(EVENT.SETTINGS_UPDATE, settings => this._onSettingsUpdate(settings)); + } + + connectedCallback() { + this._visible = false; + + if (this.hasAttribute('visible')) { + this.show(); + } else { + this.hide(); + } + + document.addEventListener('keydown', event => { + if (!shouldHandle(this.view)) { + return; + } + + if (event.code === 'Escape' && this._visible) { + this.hide(); + event.preventDefault(); + } + }); + + this.render(); + } + + get view() { + return this._view; + } + + set view(view) { + this._view = view; + return this._view; + } + + show() { + if (this._visible) { + return; + } + + this.view = viewIn(); + + requestAnimationFrame(() => { + this._visible = true; + this.classList.add('shown'); + this.removeAttribute('visible'); + this.setAttribute('aria-hidden', false); + }); + } + + hide() { + if (!this._visible) { + return; + } + + viewOut(this.view); + + requestAnimationFrame(() => { + this._visible = false; + this.classList.remove('shown'); + this.setAttribute('visible', true); + this.setAttribute('aria-hidden', true); + }); + } + + _onSettingsUpdate(settings) { + this._settings = Object.assign({}, this._settings, settings); + this.render(); + } + + _onSettingChange(setting, value) { + this._settings[setting] = value; + + dispatch(EVENT.SETTINGS_CHANGE, this._settings); + + // this.render(); + } + + render() { + this.html` +
+
+

Settings

+
+ + +
+
+ +
+
+ `; + } +} + +customElements.define('clippy-settings', ClippySettings); + +export default ClippySettings; diff --git a/src/renderer/components/clippy-switch/clippy-switch.css b/src/renderer/components/clippy-switch/clippy-switch.css new file mode 100644 index 0000000..ba331bc --- /dev/null +++ b/src/renderer/components/clippy-switch/clippy-switch.css @@ -0,0 +1,80 @@ +clippy-switch { + display: block; +} + +clippy-switch .switch-container { + padding: 0.25rem 0; + width: 100%; + display: flex; + align-items: center; +} + +clippy-switch .switch-container input[type='checkbox'] { + display: none; +} + +clippy-switch .switch-container .switch { + display: inline-block; + width: 2.25rem; + height: 1.25em; + position: relative; +} + +clippy-switch .switch-container .switch .track { + border-radius: 1.25em; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0.25rem; + background: rgba(0, 0, 0, 0.16); +} + +clippy-switch .switch-container .switch .handle { + position: absolute; + top: 0; + left: 0; + width: 1.25rem; + height: 1.25rem; + border-radius: 100%; + background: #aaa; + overflow: hidden; + transition: transform 0.15s ease-out; + will-change: transform; +} + +clippy-switch .switch-container .switch .track::before, +clippy-switch .switch-container .switch .handle::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + transition: opacity 0.15s ease-out; + will-change: opacity; + border-radius: inherit; +} + +clippy-switch .switch-container .switch .track::before { + background: var(--color-accented-100); +} + +clippy-switch .switch-container .switch .handle::before { + background: var(--color-accented-500); +} + +clippy-switch .switch-container :checked + .switch .track::before, +clippy-switch .switch-container :checked + .switch .handle::before { + opacity: 1; +} + +clippy-switch .switch-container :checked + .switch .handle { + transform: translateX(1rem); +} + +clippy-switch .switch-container .switch-label { + padding-left: 1rem; +} diff --git a/src/renderer/components/clippy-switch/index.js b/src/renderer/components/clippy-switch/index.js new file mode 100644 index 0000000..815c1aa --- /dev/null +++ b/src/renderer/components/clippy-switch/index.js @@ -0,0 +1,53 @@ +import ClippyElement from '../clippy-element'; +import './clippy-switch.css'; + +class ClippySwitch extends ClippyElement { + connectedCallback() { + this.setAttribute('tabindex', 0); + + this.addEventListener('keyup', event => { + if (event.code === 'Space' || event.code === 'Enter') { + this.selected = !this.selected; + } + }); + + this.render(); + } + + render() { + this.html` + + `; + + this.input = this.querySelector('input'); + } + + attributeChangedCallback(attr, previousValue, currentValue) { + if (attr === 'selected') { + this.render(); + } + } + + static get observedAttributes() { + return ['selected']; + } + + get selected() { + return this.getAttribute('selected') === 'true'; + } + + set selected(value) { + this.setAttribute('selected', value); + } +} + +customElements.define('clippy-switch', ClippySwitch); + +export default ClippySwitch; diff --git a/src/renderer/components/clippy-toolbar.html b/src/renderer/components/clippy-toolbar.html deleted file mode 100644 index aa46655..0000000 --- a/src/renderer/components/clippy-toolbar.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - diff --git a/src/renderer/components/material-switch.html b/src/renderer/components/material-switch.html deleted file mode 100644 index f3e219b..0000000 --- a/src/renderer/components/material-switch.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/src/renderer/fonts/noto/bold/latin-ext.woff2 b/src/renderer/fonts/noto/bold/latin-ext.woff2 deleted file mode 100644 index 63d06ff..0000000 Binary files a/src/renderer/fonts/noto/bold/latin-ext.woff2 and /dev/null differ diff --git a/src/renderer/fonts/noto/bold/latin.woff2 b/src/renderer/fonts/noto/bold/latin.woff2 deleted file mode 100644 index 72744d5..0000000 Binary files a/src/renderer/fonts/noto/bold/latin.woff2 and /dev/null differ diff --git a/src/renderer/fonts/noto/regular/latin-ext.woff2 b/src/renderer/fonts/noto/regular/latin-ext.woff2 deleted file mode 100644 index 69fd6d0..0000000 Binary files a/src/renderer/fonts/noto/regular/latin-ext.woff2 and /dev/null differ diff --git a/src/renderer/fonts/noto/regular/latin.woff2 b/src/renderer/fonts/noto/regular/latin.woff2 deleted file mode 100644 index 4b3b066..0000000 Binary files a/src/renderer/fonts/noto/regular/latin.woff2 and /dev/null differ diff --git a/src/renderer/img/clip-128x128.png b/src/renderer/img/clip-128x128.png deleted file mode 100644 index d65023f..0000000 Binary files a/src/renderer/img/clip-128x128.png and /dev/null differ diff --git a/src/renderer/img/clip-256x256.png b/src/renderer/img/clip-256x256.png deleted file mode 100644 index de3c3db..0000000 Binary files a/src/renderer/img/clip-256x256.png and /dev/null differ diff --git a/src/renderer/img/clip-32x32.png b/src/renderer/img/clippy-32.png similarity index 100% rename from src/renderer/img/clip-32x32.png rename to src/renderer/img/clippy-32.png diff --git a/src/renderer/index.html b/src/renderer/index.html index ad6584c..932ca59 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -3,31 +3,11 @@ - - - + + Clippy - - - - - - - - - + - - - -
- - -
- - Run at startup - -
- + diff --git a/src/renderer/renderer.css b/src/renderer/renderer.css new file mode 100644 index 0000000..86e647f --- /dev/null +++ b/src/renderer/renderer.css @@ -0,0 +1,89 @@ +:root { + --color-primary-900: #212121; + --color-primary-800: #424242; + --color-primary-700: #616161; + --color-primary-600: #757575; + --color-primary-500: #9e9e9e; + --color-primary-200: #eee; + --color-primary-100: #f5f5f5; + --color-primary-50: #fafafa; + + --color-accented-700: #7b1fa2; + --color-accented-500: #9c27b0; + --color-accented-300: #ba68c8; + --color-accented-100: #e1bee7; + --color-accented-50: #f3e5f5; + + --color-item-active-fg: #fff; + --color-item-active-bg: var(--color-accented-500); + + --color-scrollbar: var(--color-accented-300); + --color-scrollbar-track: var(--color-accented-100); + --color-scrollbar-corner: var(--color-accented-100); +} + +* { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +*::before, *::after { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + box-sizing: inherit; +} + +::selection { + background-color: var(--color-accented-100, #b3d4fc); +} + +html, body { + margin: 0; + padding: 0; + font-family: system-ui, sans-serif; + color: var(--color-primary-900); + font-size: 16px; + line-height: 1.5; + background: transparent; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +strong, +h1, h2, h3, h4, h5, h6 { + font-weight: 500; +} + +::-webkit-scrollbar { + height: 3px; + width: 3px; +} + +::-webkit-scrollbar-track { + background: var(--color-scrollbar-track); +} + +::-webkit-scrollbar-thumb { + background: var(--color-scrollbar); +} + +::-webkit-scrollbar-corner { + background: var(--color-scrollbar-corner); +} + +button { + color: var(--color-accented-500); + background: var(--color-accented-50); + border: none; + border-radius: 0.25rem; + font: inherit; + font-weight: 500; + padding: 0.25rem 1rem; + text-transform: uppercase; +} + +button[disabled] { + filter: grayscale(); + cursor: not-allowed; +} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js new file mode 100644 index 0000000..adc8a9e --- /dev/null +++ b/src/renderer/renderer.js @@ -0,0 +1,23 @@ +import {ipcRenderer} from 'electron'; +import {bind} from 'hyperhtml/esm'; +import {subscribe, dispatch} from 'global-dispatcher'; +import {EVENT} from '../constants'; +import './components/clippy-app'; +import './renderer.css'; + +bind(document.body)``; + +// some back and forth event handlers +// between main and render processes +ipcRenderer.on(EVENT.ITEM_NEW, (event, item) => dispatch(EVENT.ITEM_NEW, item)); + +subscribe(EVENT.ITEMS_SAVE, data => ipcRenderer.send(EVENT.ITEMS_SAVE, data)); +ipcRenderer.on(EVENT.ITEMS_CLEAR, () => dispatch(EVENT.ITEMS_CLEAR)); +ipcRenderer.on(EVENT.ITEMS_RESTORE, (event, items) => dispatch(EVENT.ITEMS_RESTORE, items)); + +ipcRenderer.on(EVENT.SETTINGS_UPDATE, (event, settings) => dispatch(EVENT.SETTINGS_UPDATE, settings)); +subscribe(EVENT.SETTINGS_CHANGE, settings => ipcRenderer.send(EVENT.SETTINGS_CHANGE, settings)); + +subscribe(EVENT.APP_HIDE, () => ipcRenderer.send(EVENT.APP_HIDE)); + +window.addEventListener('load', () => ipcRenderer.send(EVENT.APP_INIT)); diff --git a/src/renderer/stylesheets/fonts.css b/src/renderer/stylesheets/fonts.css deleted file mode 100644 index 8dfa518..0000000 --- a/src/renderer/stylesheets/fonts.css +++ /dev/null @@ -1,35 +0,0 @@ -/* latin-ext */ -@font-face { - font-family: 'Noto Sans'; - font-style: normal; - font-weight: 400; - src: local('Noto Sans'), local('NotoSans'), url(../fonts/noto/regular/latin-ext.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Noto Sans'; - font-style: normal; - font-weight: 400; - src: local('Noto Sans'), local('NotoSans'), url(../fonts/noto/regular/latin.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; -} - - - -/* latin-ext */ -@font-face { - font-family: 'Noto Sans'; - font-style: normal; - font-weight: 700; - src: local('Noto Sans Bold'), local('NotoSans-Bold'), url(../fonts/noto/bold/latin-ext.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Noto Sans'; - font-style: normal; - font-weight: 700; - src: local('Noto Sans Bold'), local('NotoSans-Bold'), url(../fonts/noto/bold/latin.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; -} diff --git a/src/renderer/stylesheets/main.css b/src/renderer/stylesheets/main.css deleted file mode 100644 index b07fa5e..0000000 --- a/src/renderer/stylesheets/main.css +++ /dev/null @@ -1,45 +0,0 @@ -:root { - --color-primary-1: #424242; - --color-primary-2: #616161; - --color-primary-3: #757575; - --color-primary-4: #fafafa; - - --color-accented: #9C27B0; - --color-accented--light: #E1BEE7; - --color-foreground: #fff; - --color-scrollbar: var(--color-accented); -} - -html { - box-sizing: border-box; -} - -*, *::before, *::after { - -webkit-tap-highlight-color: rgba(0,0,0,0); - box-sizing: inherit; - user-select: none; - cursor: default; -} - -html, -body { - display: flex; - width: 100vw; - height: 100vh; -} - -body { - margin: 0; - font-family: "Noto Sans", system-ui, sans-serif; - color: var(--color-primary-1); - font-size: 16px; - line-height: 1.5; - flex-direction: column; - background: transparent; -} - -.panes-container { - display: flex; - height: 100%; - width: 100%; -} diff --git a/src/renderer/util/index.js b/src/renderer/util/index.js new file mode 100644 index 0000000..116c825 --- /dev/null +++ b/src/renderer/util/index.js @@ -0,0 +1,11 @@ +export const clamp = (num, start = 0, end = Number.MIN_SAFE_INTEGER) => { + if (num < start) { + return start; + } + + if (num > end) { + return end; + } + + return num; +}; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..8e0611b --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,86 @@ +const path = require('path'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const sourcePath = path.resolve(__dirname, 'src'); +const buildPath = path.resolve(__dirname, 'build'); + +const IS_PROD = process.env.NODE_ENV === 'production'; + +const pathsToClean = [ + path.resolve(buildPath, 'main'), + path.resolve(buildPath, 'renderer') +]; + +const commonConfig = { + output: { + path: buildPath, + filename: '[name].js' + }, + + // Disable mocking node globals + node: false, + + devtool: IS_PROD ? false : 'source-map', + mode: IS_PROD ? 'production' : 'development' +}; + +module.exports = [ + { + target: 'electron-main', + entry: { + 'main/main': path.resolve(sourcePath, 'main/main.js') + }, + ...commonConfig + }, { + target: 'electron-renderer', + entry: { + 'renderer/renderer': path.resolve(sourcePath, 'renderer/renderer.js') + }, + + module: { + rules: [ + { + test: /\.css$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + sourceMap: !IS_PROD + } + } + ] + } + ] + }, + + plugins: [ + new CleanWebpackPlugin(pathsToClean, { + verbose: true + }), + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[id].css' + }), + new HtmlWebpackPlugin({ + template: path.resolve(sourcePath, 'renderer/index.html'), + filename: path.resolve(buildPath, 'renderer/index.html') + }), + new CopyWebpackPlugin([ + { + from: path.resolve(sourcePath, 'renderer/img/*.png'), + to: path.resolve(buildPath, 'renderer/img/'), + flatten: true, + transform(content) { + // TODO: Maybe optimize images here + return content; + } + } + ]) + ], + ...commonConfig + } +];