├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── _config.yml ├── browser.css ├── browser.js ├── build ├── background.png ├── background@2x.png └── entitlements.mas.plist ├── config.js ├── dark-mode.css ├── index.js ├── media ├── dark.png └── light.png ├── menu.js ├── package.json ├── readme.md └── static ├── Icon.png ├── IconTray.png ├── IconTray@2x.png └── passerine.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | /dist 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode9.2 3 | language: node_js 4 | node_js: '8' 5 | script: 6 | - npm test 7 | - npm run dist 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at thabeatsz@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /browser.css: -------------------------------------------------------------------------------- 1 | html {/* 2 | overflow: hidden;*/ 3 | } 4 | 5 | /* Add OS-specific fonts */ 6 | body { 7 | font-family: -apple-system, 8 | BlinkMacSystemFont, 9 | 'Segoe UI', 10 | Roboto, 11 | Oxygen-Sans, 12 | Ubuntu, 13 | Cantarell, 14 | 'Helvetica Neue', 15 | sans-serif, 16 | 'Apple Color Emoji', 17 | 'Segoe UI Emoji', 18 | 'Segoe UI Symbol' !important; 19 | text-rendering: optimizeLegibility !important; 20 | font-feature-settings: 'liga', 'clig', 'kern'; 21 | } 22 | 23 | 24 | .secondaryContent_cdfcd{ 25 | display: none; 26 | } 27 | 28 | 29 | @media all and (max-width: 700px) { 30 | /* Make right column flexible in compact mode */ 31 | ._1q5- { 32 | position: absolute !important; 33 | right: 0 !important; 34 | left: 74px !important; 35 | height: 100% !important; 36 | } 37 | 38 | /* Fix for left space in compact mode */ 39 | html.sidebar-hidden ._1q5- { 40 | left: 0px !important; 41 | } 42 | 43 | /* `New Conversation` icon */ 44 | ._30yy[href='/new'] { 45 | display: none !important; 46 | } 47 | 48 | /* Hide right sidebar in compact mode */ 49 | ._4_j5 { 50 | display: none !important; 51 | } 52 | } 53 | 54 | /* Don't show outline on clickable elements except in the delete/mute modal so that we know what we are selecting */ 55 | a, 56 | *[role='button'] { 57 | outline: none !important; 58 | } 59 | 60 | html .customizeLink_98c2c{ 61 | visibility: hidden; 62 | } 63 | 64 | 65 | /* hide navbar */ 66 | html .navigation_fe7df{ 67 | visibility: hidden; 68 | } -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const config = require('./config'); 4 | 5 | const {ipcRenderer: ipc} = electron; 6 | 7 | function setDarkMode() { 8 | document.documentElement.classList.toggle('dark-mode', config.get('darkMode')); 9 | ipc.send('set-vibrancy'); 10 | } 11 | 12 | function setVibrancy() { 13 | document.documentElement.classList.toggle('vibrancy', config.get('vibrancy')); 14 | ipc.send('set-vibrancy'); 15 | } 16 | 17 | function renderOverlayIcon(messageCount) { 18 | const canvas = document.createElement('canvas'); 19 | canvas.height = 128; 20 | canvas.width = 128; 21 | canvas.style.letterSpacing = '-5px'; 22 | const ctx = canvas.getContext('2d'); 23 | ctx.fillStyle = '#f42020'; 24 | ctx.beginPath(); 25 | ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI); 26 | ctx.fill(); 27 | ctx.textAlign = 'center'; 28 | ctx.fillStyle = 'white'; 29 | ctx.font = '90px sans-serif'; 30 | ctx.fillText(String(Math.min(99, messageCount)), 64, 96); 31 | return canvas; 32 | } 33 | 34 | // eslint-disable-line capitalized-comments 35 | // ipc.on('toggle-sidebar', () => { 36 | // config.set('sidebarHidden', !config.get('sidebarHidden')); 37 | // setSidebarVisibility(); 38 | // }); 39 | 40 | ipc.on('toggle-dark-mode', () => { 41 | config.set('darkMode', !config.get('darkMode')); 42 | setDarkMode(); 43 | }); 44 | 45 | ipc.on('toggle-vibrancy', () => { 46 | config.set('vibrancy', !config.get('vibrancy')); 47 | setVibrancy(); 48 | 49 | document.documentElement.style.backgroundColor = 'transparent'; 50 | }); 51 | 52 | // Inject a global style node to maintain custom appearance after conversation change or startup 53 | document.addEventListener('DOMContentLoaded', () => { 54 | const style = document.createElement('style'); 55 | style.id = 'zoomFactor'; 56 | document.body.appendChild(style); 57 | 58 | // Set the zoom factor if it was set before quitting 59 | const zoomFactor = config.get('zoomFactor') || 1.0; 60 | setZoom(zoomFactor); 61 | 62 | // Hide sidebar if it was hidden before quitting 63 | setSidebarVisibility(); 64 | 65 | // Activate Dark Mode if it was set before quitting 66 | setDarkMode(); 67 | 68 | // Prevent flash of white on startup when in dark mode 69 | // TODO: find a CSS-only solution 70 | if (config.get('darkMode')) { 71 | document.documentElement.style.backgroundColor = '#192633'; 72 | } 73 | 74 | // Activate vibrancy effect if it was set before quitting 75 | setVibrancy(); 76 | }); 77 | 78 | // It's not possible to add multiple accelerators 79 | // so this needs to be done the old-school way 80 | document.addEventListener('keydown', event => { 81 | const combineKey = process.platform === 'darwin' ? event.metaKey : event.ctrlKey; 82 | 83 | if (!combineKey) { 84 | return; 85 | } 86 | 87 | if (event.key === ']') { 88 | nextConversation(); 89 | } 90 | 91 | if (event.key === '[') { 92 | previousConversation(); 93 | } 94 | 95 | const num = parseInt(event.key, 10); 96 | 97 | if (num >= 1 && num <= 9) { 98 | jumpToConversation(num); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/build/background.png -------------------------------------------------------------------------------- /build/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/build/background@2x.png -------------------------------------------------------------------------------- /build/entitlements.mas.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.files.downloads.read-write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Store = require('electron-store'); 3 | 4 | module.exports = new Store({ 5 | defaults: { 6 | darkMode: false, 7 | vibrancy: false, 8 | zoomFactor: 1, 9 | lastWindowState: { 10 | width: 800, 11 | height: 600 12 | }, 13 | alwaysOnTop: false, 14 | bounceDockOnMessage: false, 15 | showUnreadBadge: true, 16 | launchMinimized: false, 17 | flashWindowOnMessage: true, 18 | block: { 19 | chatSeen: false, 20 | typingIndicator: false 21 | }, 22 | confirmImagePaste: true, 23 | useWorkChat: false, 24 | sidebarHidden: false, 25 | autoHideMenuBar: false, 26 | notificationsMuted: false 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /dark-mode.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base: black; 3 | --base-ninety: rgba(255, 255, 255, 0.9); 4 | --base-seventy-five: rgba(255, 255, 255, 0.75); 5 | --base-seventy: rgba(255, 255, 255, 0.7); 6 | --base-fifty: rgba(255, 255, 255, 0.5); 7 | --base-fourty: rgba(255, 255, 255, 0.4); 8 | --base-thiry: rgba(255, 255, 255, 0.3); 9 | --base-twenty: rgba(255, 255, 255, 0.2); 10 | --base-five: rgba(255, 255, 255, 0.05); 11 | --base-ten: rgba(255, 255, 255, 0.1); 12 | --base-nine: rgba(255, 255, 255, 0.09); 13 | --container-color: #192633; 14 | --conversation-bg: rgb(25, 38, 51); 15 | --list-header-color: #202C3A; 16 | --blue: #0084ff; 17 | } 18 | 19 | html.dark-mode body { 20 | color: var(--base-seventy); 21 | background: var(--container-color) !important; 22 | } 23 | 24 | html .dark-mode a:hover { 25 | color: var(--container-color) !important; 26 | } 27 | 28 | html .dark-mode .hero_4c01e a:hover { 29 | color: var(--container-color) !important; 30 | } 31 | 32 | html.dark-mode .productRequest_82e25 a:hover { 33 | background: transparent !important; 34 | } 35 | 36 | /* Main container? */ 37 | html.dark-mode .hero_4c01e { 38 | background: var(--container-color) !important; 39 | } 40 | 41 | /* header container? */ 42 | html.dark-mode .header_0a28e { 43 | background: var(--container-color) !important; 44 | } 45 | 46 | 47 | /* Card container? */ 48 | html.dark-mode .productRequest_82e25{ 49 | background: var(--container-color) !important; 50 | } 51 | 52 | 53 | 54 | 55 | /* Card container? */ 56 | html.dark-mode .header_fb2c3{ 57 | background: var(--container-color) !important; 58 | } 59 | 60 | /* Card container? */ 61 | html.dark-mode .headerWithTitleAndChildren_d144a .header_fb2c3{ 62 | background: var(--container-color) !important; 63 | } 64 | 65 | html.dark-mode .white_09016.border_64d93.margin_1b96e{ 66 | background: var(--container-color) !important; 67 | } 68 | 69 | 70 | 71 | 72 | 73 | /* Card container? */ 74 | html.dark-mode .white_09016.border_64d93.margin_1b96e{ 75 | /*border-color: black;*/ 76 | background: var(--base-five); 77 | } 78 | 79 | 80 | html.dark-mode .item_54fdd a:hover { 81 | background: transparent !important; 82 | } 83 | 84 | html.dark-mode .list_0372b a:hover { 85 | background: transparent !important; 86 | } 87 | 88 | 89 | html.dark-mode .item_6a520 a:hover { 90 | background: transparent !important; 91 | } 92 | 93 | 94 | html.dark-mode .item_1523a.activeItem_c89bf.item_1523a{ 95 | /*border-color: black;*/ 96 | background: var(--base-five); 97 | } 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | /* Card container? */ 106 | html.dark-mode .font_9d927.grey_bbe43.xSmall_1a46e.normal_d2e66.topic_ca358.button_53e93.uppercase_a49b4{ 107 | color: black !important; 108 | } 109 | 110 | /* Card container? */ 111 | html.dark-mode .postsList_b2208{ 112 | background: var(--container-color) !important; 113 | } 114 | 115 | html.dark-mode .button_30e5c.smallSize_5216f.subtleVariant_42ccf.simpleVariant_8a863.button_e47d2{ 116 | color: black !important; 117 | } 118 | 119 | html.dark-mode .font_9d927.black_476ed.small_231df.normal_d2e66.itemText_063f9{ 120 | color: white !important; 121 | } 122 | 123 | 124 | 125 | html.dark-mode .font_9d927.black_476ed.xLarge_18016.bold_f69ef.title_88ce7{ 126 | color: white !important; 127 | } 128 | 129 | html.dark-mode .font_9d927.black_476ed.large_db9e7.light_4a7e0.title_39c87{ 130 | color: white !important; 131 | } 132 | 133 | 134 | 135 | html.dark-mode .font_9d927.black_476ed.large_db9e7.light_4a7e0.title_9ddaf{ 136 | color: white !important; 137 | } 138 | 139 | 140 | html.dark-mode .mask_98dce{ 141 | visibility: hidden; 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const Url = require('url').URL; 5 | const electron = require('electron'); 6 | // -const electronLocalShortcut = require('electron-localshortcut'); 7 | const log = require('electron-log'); 8 | const {autoUpdater} = require('electron-updater'); 9 | const isDev = require('electron-is-dev'); 10 | const appMenu = require('./menu'); 11 | const config = require('./config'); 12 | // -const tray = require('./tray'); 13 | 14 | require('electron-debug')({enabled: true}); 15 | require('electron-dl')(); 16 | require('electron-context-menu')(); 17 | 18 | const domain = 'producthunt.com'; 19 | const {app, ipcMain} = electron; 20 | 21 | app.setAppUserModelId('com.gauthamzz.legacy'); 22 | app.disableHardwareAcceleration(); 23 | 24 | if (!isDev) { 25 | autoUpdater.logger = log; 26 | autoUpdater.logger.transports.file.level = 'info'; 27 | autoUpdater.checkForUpdates(); 28 | } 29 | 30 | let mainWindow; 31 | let isQuitting = false; 32 | let prevMessageCount = 0; 33 | let dockMenu; 34 | 35 | const isAlreadyRunning = app.makeSingleInstance(() => { 36 | if (mainWindow) { 37 | if (mainWindow.isMinimized()) { 38 | mainWindow.restore(); 39 | } 40 | 41 | mainWindow.show(); 42 | } 43 | }); 44 | 45 | if (isAlreadyRunning) { 46 | app.quit(); 47 | } 48 | 49 | function createMainWindow() { 50 | const lastWindowState = config.get('lastWindowState'); 51 | const isDarkMode = config.get('darkMode'); 52 | // Messenger or Work Chat 53 | const mainURL = 'https://www.producthunt.com'; 54 | const titlePrefix = 'Product Hunt'; 55 | 56 | const win = new electron.BrowserWindow({ 57 | title: app.getName(), 58 | show: false, 59 | x: lastWindowState.x, 60 | y: lastWindowState.y, 61 | width: lastWindowState.width, 62 | height: lastWindowState.height, 63 | icon: process.platform === 'linux' && path.join(__dirname, 'static/Icon.png'), 64 | minWidth: 400, 65 | minHeight: 200, 66 | alwaysOnTop: config.get('alwaysOnTop'), 67 | // Temp workaround for macOS High Sierra, see #295 68 | titleBarStyle: process.platform === 'darwin' && Number(require('os').release().split('.')[0]) >= 17 ? null : 'hidden-inset', 69 | autoHideMenuBar: config.get('autoHideMenuBar'), 70 | darkTheme: isDarkMode, // GTK+3 71 | webPreferences: { 72 | preload: path.join(__dirname, 'browser.js'), 73 | nodeIntegration: false, 74 | plugins: true 75 | } 76 | }); 77 | 78 | if (process.platform === 'darwin') { 79 | win.setSheetOffset(40); 80 | } 81 | 82 | win.loadURL(mainURL); 83 | 84 | win.on('close', e => { 85 | if (!isQuitting) { 86 | e.preventDefault(); 87 | 88 | // Workaround for electron/electron#10023 89 | win.blur(); 90 | 91 | if (process.platform === 'darwin') { 92 | app.hide(); 93 | } else { 94 | win.hide(); 95 | } 96 | } 97 | }); 98 | 99 | win.on('page-title-updated', (e, title) => { 100 | e.preventDefault(); 101 | }); 102 | 103 | win.on('focus', () => { 104 | if (config.get('flashWindowOnMessage')) { 105 | // This is a security in the case where messageCount is not reset by page title update 106 | win.flashFrame(false); 107 | } 108 | }); 109 | 110 | return win; 111 | } 112 | 113 | app.on('ready', () => { 114 | const trackingUrlPrefix = `https://l.${domain}/l.php`; 115 | electron.Menu.setApplicationMenu(appMenu); 116 | mainWindow = createMainWindow(); 117 | // -tray.create(mainWindow); 118 | 119 | if (process.platform === 'darwin') { 120 | dockMenu = electron.Menu.buildFromTemplate([ 121 | { 122 | label: 'Mute Notifications', 123 | type: 'checkbox', 124 | checked: config.get('notificationsMuted'), 125 | click() { 126 | mainWindow.webContents.send('toggle-mute-notifications'); 127 | } 128 | } 129 | ]); 130 | app.dock.setMenu(dockMenu); 131 | } 132 | 133 | const {webContents} = mainWindow; 134 | 135 | webContents.on('dom-ready', () => { 136 | webContents.insertCSS(fs.readFileSync(path.join(__dirname, 'browser.css'), 'utf8')); 137 | webContents.insertCSS(fs.readFileSync(path.join(__dirname, 'dark-mode.css'), 'utf8')); 138 | 139 | if (config.get('launchMinimized')) { 140 | mainWindow.hide(); 141 | } else { 142 | mainWindow.show(); 143 | } 144 | 145 | mainWindow.webContents.send('toggle-mute-notifications', config.get('notificationsMuted')); 146 | }); 147 | 148 | webContents.on('new-window', (event, url, frameName, options) => { 149 | event.preventDefault(); 150 | 151 | if (url === 'about:blank') { 152 | if (frameName !== 'about:blank') { // Voice/video call popup 153 | options.show = true; 154 | options.titleBarStyle = 'default'; 155 | options.webPreferences.nodeIntegration = false; 156 | options.webPreferences.preload = path.join(__dirname, 'browser-call.js'); 157 | event.newGuest = new electron.BrowserWindow(options); 158 | } 159 | } else { 160 | if (url.startsWith(trackingUrlPrefix)) { 161 | url = new Url(url).searchParams.get('u'); 162 | } 163 | 164 | electron.shell.openExternal(url); 165 | } 166 | }); 167 | }); 168 | 169 | ipcMain.on('set-vibrancy', () => { 170 | if (config.get('vibrancy')) { 171 | mainWindow.setVibrancy(config.get('darkMode') ? 'ultra-dark' : 'light'); 172 | } else { 173 | mainWindow.setVibrancy(null); 174 | } 175 | }); 176 | 177 | ipcMain.on('mute-notifications-toggled', (event, status) => { 178 | setNotificationsMute(status); 179 | }); 180 | 181 | app.on('activate', () => { 182 | mainWindow.show(); 183 | }); 184 | 185 | app.on('before-quit', () => { 186 | isQuitting = true; 187 | 188 | if (!mainWindow.isFullScreen()) { 189 | config.set('lastWindowState', mainWindow.getBounds()); 190 | } 191 | }); 192 | -------------------------------------------------------------------------------- /media/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/media/dark.png -------------------------------------------------------------------------------- /media/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/media/light.png -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const os = require('os'); 3 | const path = require('path'); 4 | const electron = require('electron'); 5 | const config = require('./config'); 6 | 7 | const {app, BrowserWindow, shell} = electron; 8 | const appName = app.getName(); 9 | 10 | function sendAction(action) { 11 | const [win] = BrowserWindow.getAllWindows(); 12 | 13 | if (process.platform === 'darwin') { 14 | win.restore(); 15 | } 16 | 17 | win.webContents.send(action); 18 | } 19 | 20 | const viewSubmenu = [ 21 | 22 | { 23 | label: 'Toggle Dark Mode', 24 | accelerator: 'CmdOrCtrl+D', 25 | click() { 26 | sendAction('toggle-dark-mode'); 27 | } 28 | } 29 | ]; 30 | 31 | const helpSubmenu = [ 32 | { 33 | label: 'Source Code', 34 | click() { 35 | shell.openExternal('https://github.com/aviary-apps/Passerine'); 36 | } 37 | }, 38 | { 39 | label: 'Report an Issue…', 40 | click() { 41 | const body = ` 42 | 43 | 44 | 45 | --- 46 | 47 | ${app.getName()} ${app.getVersion()} 48 | Electron ${process.versions.electron} 49 | ${process.platform} ${process.arch} ${os.release()}`; 50 | 51 | shell.openExternal(`https://github.com/aviary-apps/Passerine/issues/new?body=${encodeURIComponent(body)}`); 52 | } 53 | } 54 | ]; 55 | 56 | if (process.platform === 'darwin') { 57 | viewSubmenu.push({ 58 | label: 'Toggle Vibrancy', 59 | click() { 60 | sendAction('toggle-vibrancy'); 61 | } 62 | }); 63 | } else { 64 | helpSubmenu.push({ 65 | type: 'separator' 66 | }, { 67 | role: 'about', 68 | click() { 69 | electron.dialog.showMessageBox({ 70 | title: `About ${appName}`, 71 | message: `${appName} ${app.getVersion()}`, 72 | detail: 'Created by Aviary Apps', 73 | icon: path.join(__dirname, 'static/Icon.png') 74 | }); 75 | } 76 | }); 77 | 78 | viewSubmenu.push({ 79 | type: 'separator' 80 | }, { 81 | type: 'checkbox', 82 | label: 'Always on Top', 83 | accelerator: 'Ctrl+Shift+T', 84 | checked: config.get('alwaysOnTop'), 85 | click(item, focusedWindow) { 86 | config.set('alwaysOnTop', item.checked); 87 | focusedWindow.setAlwaysOnTop(item.checked); 88 | } 89 | }); 90 | } 91 | 92 | const macosTpl = [ 93 | { 94 | label: appName, 95 | submenu: [ 96 | { 97 | role: 'about' 98 | }, 99 | { 100 | role: 'quit' 101 | } 102 | ] 103 | }, 104 | { 105 | label: 'File', 106 | submenu: [ 107 | 108 | ] 109 | }, 110 | { 111 | role: 'editMenu' 112 | }, 113 | { 114 | label: 'View', 115 | submenu: viewSubmenu 116 | }, 117 | { 118 | role: 'window', 119 | submenu: [ 120 | { 121 | role: 'minimize' 122 | }, 123 | { 124 | role: 'close' 125 | }, 126 | { 127 | type: 'separator' 128 | }, 129 | { 130 | role: 'front' 131 | }, 132 | { 133 | role: 'togglefullscreen' 134 | }, 135 | { 136 | type: 'separator' 137 | }, 138 | { 139 | type: 'checkbox', 140 | label: 'Always on Top', 141 | accelerator: 'Cmd+Shift+T', 142 | checked: config.get('alwaysOnTop'), 143 | click(item, focusedWindow) { 144 | config.set('alwaysOnTop', item.checked); 145 | focusedWindow.setAlwaysOnTop(item.checked); 146 | } 147 | } 148 | ] 149 | }, 150 | { 151 | role: 'help', 152 | submenu: helpSubmenu 153 | } 154 | ]; 155 | 156 | const otherTpl = [ 157 | { 158 | label: 'File', 159 | submenu: [ 160 | 161 | { 162 | type: 'separator' 163 | }, 164 | { 165 | role: 'quit' 166 | } 167 | ] 168 | }, 169 | { 170 | label: 'Edit', 171 | submenu: [ 172 | { 173 | role: 'undo' 174 | }, 175 | { 176 | role: 'redo' 177 | }, 178 | { 179 | type: 'separator' 180 | }, 181 | { 182 | role: 'cut' 183 | }, 184 | { 185 | role: 'copy' 186 | }, 187 | { 188 | role: 'paste' 189 | }, 190 | { 191 | role: 'delete' 192 | }, 193 | { 194 | type: 'separator' 195 | }, 196 | { 197 | role: 'selectall' 198 | }, 199 | { 200 | type: 'separator' 201 | } 202 | ] 203 | }, 204 | { 205 | label: 'View', 206 | submenu: viewSubmenu 207 | }, 208 | { 209 | role: 'help', 210 | submenu: helpSubmenu 211 | } 212 | ]; 213 | 214 | const tpl = process.platform === 'darwin' ? macosTpl : otherTpl; 215 | 216 | module.exports = electron.Menu.buildFromTemplate(tpl); 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passerine", 3 | "productName": "Passerine", 4 | "version": "1.0.1", 5 | "description": "Product Hunt desktop app", 6 | "license": "MIT", 7 | "repository": "github.com/aviary-apps/Passerine", 8 | "author": { 9 | "name": "Aviary Apps", 10 | "email": "thabeatsz@gmail.com", 11 | "url": "gauthamzz.com" 12 | }, 13 | "scripts": { 14 | "postinstall": "electron-builder install-app-deps", 15 | "test": "xo", 16 | "start": "electron .", 17 | "pack": "electron-builder --dir", 18 | "dist": "electron-builder --mac --linux --win" 19 | }, 20 | "dependencies": { 21 | "electron-context-menu": "^0.9.1", 22 | "electron-debug": "^1.4.0", 23 | "electron-dl": "^1.0.0", 24 | "electron-is-dev": "^0.3.0", 25 | "electron-localshortcut": "^2.0.0", 26 | "electron-log": "^2.0.2", 27 | "electron-store": "^1.1.0", 28 | "electron-updater": "^2.18.2", 29 | "element-ready": "^2.2.0", 30 | "facebook-locales": "^1.0.464" 31 | }, 32 | "devDependencies": { 33 | "electron": "1.7.11", 34 | "electron-builder": "19.54.0", 35 | "xo": "*" 36 | }, 37 | "xo": { 38 | "envs": [ 39 | "node", 40 | "browser" 41 | ] 42 | }, 43 | "build": { 44 | "files": [ 45 | "**/*", 46 | "!media${/*}" 47 | ], 48 | "appId": "com.aviary-apps.passerine", 49 | "mac": { 50 | "category": "public.app-category.social-networking", 51 | "electronUpdaterCompatibility": ">=2.18.2" 52 | }, 53 | "dmg": { 54 | "iconSize": 160, 55 | "contents": [ 56 | { 57 | "x": 180, 58 | "y": 170 59 | }, 60 | { 61 | "x": 480, 62 | "y": 170, 63 | "type": "link", 64 | "path": "/Applications" 65 | } 66 | ] 67 | }, 68 | "linux": { 69 | "target": [ 70 | "AppImage", 71 | "deb" 72 | ] 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Passerine 2 | 3 | > Product Hunt desktop app :rocket: 4 | 5 | Passerine is a desktop version of Product Hunt :rocket:. 6 | 7 | **[Website](https://aviary-apps.github.io/passerine)**    **[Releases](https://github.com/aviary-apps/Passerine/releases)** 8 | 9 |
10 | 11 | Buy Me A Coffee 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## Highlights 20 | 21 | - [Dark theme](#dark-mode) 22 | - [Always on Top](#always-on-top) 23 | 24 | 25 | ## Features 26 | 27 | ### Dark mode 28 | 29 | You can toggle dark mode in the `View` menu or with Cmd D / Ctrl D. 30 | 31 | 32 | 33 | ### Always on Top 34 | 35 | You can toggle whether Passerine stays on top of other windows in the `Window`/`View` menu or with Cmd/Ctrl Shift t. 36 | 37 | 38 | ### Keyboard shortcuts 39 | 40 | Description | Keys 41 | -----------------------| ----------------------- 42 | Toggle "Dark mode" | Cmd/Ctrl d 43 | Toggle "Always on Top" | Cmd/Ctrl Shift t 44 | Toggle window menu | Alt *(Windows only)* 45 | 46 | --- 47 | 48 | 49 | ## Dev 50 | 51 | Built with [Electron](https://electronjs.org). 52 | 53 | ### Run 54 | 55 | ``` 56 | $ npm install && npm start 57 | ``` 58 | 59 | ### Build 60 | 61 | See the [`electron-builder` docs](https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build). 62 | 63 | 64 | ## Disclaimer 65 | 66 | Passerine is a third-party app and is not affiliated with Product Hunt. 67 | 68 | ## Credits 69 | 70 | - [Gautham Santhosh](http://github.com/gauthamzz) 71 | - [Aswin VB](http://github.com/aswinzz) 72 | - [sindresorhus](https://github.com/sindresorhus/) and his project [caprine](https://github.com/sindresorhus/caprine)from which most of the Passerine, is inspired from. 73 | 74 | ## License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /static/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/static/Icon.png -------------------------------------------------------------------------------- /static/IconTray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/static/IconTray.png -------------------------------------------------------------------------------- /static/IconTray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/static/IconTray@2x.png -------------------------------------------------------------------------------- /static/passerine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauthamzz/Passerine/aeb759c8e8bc260a342241594578c29394fa3def/static/passerine.png --------------------------------------------------------------------------------