From d3f30ae085cccb2416a18aa480d1cade76068bcf Mon Sep 17 00:00:00 2001 From: Edenware Date: Mon, 29 Jan 2024 12:49:23 -0300 Subject: [PATCH] v1.0.3 - Improvements to UI's appearance. --- assets/custom-frame/custom-frame-core.css | 60 ++++ assets/custom-frame/custom-frame-theme.css | 28 ++ assets/custom-frame/custom-frame.js | 251 ++++++++++++++++ assets/custom-frame/icons/LICENSE.txt | 12 + assets/custom-frame/icons/README.txt | 75 +++++ assets/custom-frame/icons/css/cf-fa.css | 61 ++++ assets/custom-frame/icons/font/cf-fa.eot | Bin 0 -> 5588 bytes assets/custom-frame/icons/font/cf-fa.svg | 18 ++ assets/custom-frame/icons/font/cf-fa.ttf | Bin 0 -> 5420 bytes assets/custom-frame/icons/font/cf-fa.woff | Bin 0 -> 3144 bytes assets/custom-frame/icons/font/cf-fa.woff2 | Bin 0 -> 2508 bytes assets/scripts/events.js | 41 +++ bin-debug.js | 76 +++++ bin.js | 59 ++++ package.json | 20 +- preload.js | 329 +++++++++++++++++++++ 16 files changed, 1023 insertions(+), 7 deletions(-) create mode 100644 assets/custom-frame/custom-frame-core.css create mode 100644 assets/custom-frame/custom-frame-theme.css create mode 100644 assets/custom-frame/custom-frame.js create mode 100644 assets/custom-frame/icons/LICENSE.txt create mode 100644 assets/custom-frame/icons/README.txt create mode 100644 assets/custom-frame/icons/css/cf-fa.css create mode 100644 assets/custom-frame/icons/font/cf-fa.eot create mode 100644 assets/custom-frame/icons/font/cf-fa.svg create mode 100644 assets/custom-frame/icons/font/cf-fa.ttf create mode 100644 assets/custom-frame/icons/font/cf-fa.woff create mode 100644 assets/custom-frame/icons/font/cf-fa.woff2 create mode 100644 assets/scripts/events.js create mode 100644 bin-debug.js create mode 100644 bin.js create mode 100644 preload.js diff --git a/assets/custom-frame/custom-frame-core.css b/assets/custom-frame/custom-frame-core.css new file mode 100644 index 0000000..0b1943e --- /dev/null +++ b/assets/custom-frame/custom-frame-core.css @@ -0,0 +1,60 @@ +body { + margin:0; +} + +.cf * { + box-sizing: border-box; +} + +.cf { + position:fixed; + width:100%; + top:0; + left:0; + line-height: 100%; + box-sizing: border-box; +} + +.cf-icon { + display: inline-flex; + vertical-align: inherit; + background-position: center center; + background-repeat: no-repeat; +} + +.cf-title { + vertical-align: top; +} + +.cf-inner { + display: flex; + pointer-events: auto; +} + +.cf-handle { + flex: 1 0 0; + -webkit-app-region: drag; +} + +.cf-btn { + background: #FFF; + color: #000; + border: 0; + outline: 0; + padding: 1px .75rem; + cursor: pointer; +} + +.cf-btn:hover { + background: #FFF; + color: #000; +} + +.cf-close:hover { + background: #FFF; + color: #000; +} + +.cf-btn:focus { + outline:0; +} \ No newline at end of file diff --git a/assets/custom-frame/custom-frame-theme.css b/assets/custom-frame/custom-frame-theme.css new file mode 100644 index 0000000..dc461f6 --- /dev/null +++ b/assets/custom-frame/custom-frame-theme.css @@ -0,0 +1,28 @@ + +.cf { + position:fixed; + width:100%; + top:0; + left:0; + background: #171717; + color:#a1a1a1; + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +.cf-icon { + margin: 0 5px; +} + +.cf-btn { + background: none; + color:#a1a1a1; +} + +.cf-btn:hover { + background: #424242; +} + +.cf-close:hover { + background: #e81123; + color:#FFF; +} diff --git a/assets/custom-frame/custom-frame.js b/assets/custom-frame/custom-frame.js new file mode 100644 index 0000000..cb8addc --- /dev/null +++ b/assets/custom-frame/custom-frame.js @@ -0,0 +1,251 @@ +var _defaultAppIcon = 'default_icon.png'; +var _defaultOptions = { + 'id': 'custom-frame', + 'theme': '', + 'uiIconsTheme': '', + 'layout': 'horizontal', + 'position': 'top', + 'size': 30, + 'frameIconSize': 21, + 'classes': { + 'main': 'cf', + 'inner': 'cf-inner', + 'handle': 'cf-handle', + 'icon': 'cf-icon', + 'title': 'cf-title', + 'buttonsGroup': 'cf-buttons', + 'buttonBase': 'cf-btn', + 'buttons': { + 'minimize': 'cf-minimize', + 'maximize': 'cf-maximize', + 'restore': 'cf-restore', + 'close': 'cf-close', + }, + 'icons': { + 'minimize': 'cf-icon-minimize', + 'maximize': 'cf-icon-maximize', + 'restore': 'cf-icon-restore', + 'close': 'cf-icon-close', + } + }, + 'locales': { + 'en': { + 'close': 'Close', + 'maximize': 'Maximize', + 'restore': 'Restore', + 'minimize': 'Minimize', + }, + 'fr': { + 'close': 'Fermer', + 'maximize': 'Agrandir', + 'restore': 'Restaurer', + 'minimize': 'Réduire', + } + } +}; +var getFavicon = function (document) { + var favicon; + var nodeList = document.getElementsByTagName('link'); + for (var i = 0; i < nodeList.length; i++) { + if ((nodeList[i].getAttribute('rel') === 'icon') || (nodeList[i].getAttribute('rel') === 'shortcut icon')) { + favicon = nodeList[i].getAttribute('href'); + } + } + return favicon; +}; +class CustomFrame { + constructor(_window, options) { + this.initialized = false; + this.options = Object.assign(_defaultOptions, options); + this.window = _window; + this.document = _window.document; + } + createElement(name, attributes, styles, parentNode) { + const element = document.createElement(name) + if (attributes) { + for (const key in attributes) { + if (Object.hasOwnProperty.call(attributes, key)) { + element.setAttribute(key, attributes[key]) + } + } + } + if (styles) { + for (const key in styles) { + if (Object.hasOwnProperty.call(styles, key)) { + element.style[key] = styles[key] + } + } + } + if (parentNode) { + parentNode.appendChild(element) + } + return element + } + create() { + var that = this; + var options = this.options; + if (that.window.localStorage.customFrameState === undefined) { + that.window.localStorage.customFrameState = 'initial'; + } + var currentLocale = window.navigator.language; + var locales = options.locales[currentLocale] !== undefined ? options.locales[currentLocale] : options.locales[Object.keys(options.locales)[0]]; + var mainContainer = this.createElement('header', { id: options.id, class: options.classes.main }, { + height: Number.isInteger(options.size) ? options.size + 'px' : options.size + }) + var innerContainer = this.createElement('div', { class: 'cf-inner' }, null, mainContainer) + var handleContainer = this.createElement('div', { class: 'cf-handle' }, { height: mainContainer.style.height, lineHeight: mainContainer.style.height }, innerContainer) + var favicon + if (options.details.icon !== undefined) { + favicon = options.details.icon; + } + if (!favicon) { + favicon = getFavicon(this.document) || _defaultAppIcon; + } + var frameIcon = this.createElement('span', { class: 'cf-icon' }, { width: Number.isInteger(options.frameIconSize) ? options.frameIconSize + 'px' : options.frameIconSize, height: mainContainer.style.height, backgroundImage: 'url("' + favicon + '")' }, handleContainer); + frameIcon.style.backgroundSize = frameIcon.style.width + var titleStr; + if (this.document.getElementsByTagName('title').length !== 0) { + titleStr = this.document.title; + } else if (options.details.title !== undefined) { + titleStr = this.document.title = options.details.title; + } else { + titleStr = this.document.title = 'Custom Frame'; + } + var titleSpan = this.createElement('span', { class: options.classes.title }, null, handleContainer) + titleSpan.innerHTML = titleStr + + var buttonsContainer = this.createElement('div', { class: options.classes.buttonsGroup }, null, innerContainer); + var buttonMinimize = this.createElement('button', { class: options.classes.buttonBase + ' ' + options.classes.buttons.minimize, title: locales.minimize }, null, buttonsContainer); + var buttonMaximize = this.createElement('button', { class: options.classes.buttonBase + ' ' + options.classes.buttons.maximize, title: locales.maximize }, null, buttonsContainer); + var buttonRestore = this.createElement('button', { class: options.classes.buttonBase + ' ' + options.classes.buttons.restore, title: locales.restore }, null, buttonsContainer); + var buttonClose = this.createElement('button', { class: options.classes.buttonBase + ' ' + options.classes.buttons.close, title: locales.close }, null, buttonsContainer); + + var iconMinimize = document.createElement('i'); + iconMinimize.setAttribute('class', options.classes.icons.minimize); + buttonMinimize.appendChild(iconMinimize); + var iconMaximize = document.createElement('i'); + iconMaximize.setAttribute('class', options.classes.icons.maximize); + buttonMaximize.appendChild(iconMaximize); + var iconRestore = document.createElement('i'); + iconRestore.setAttribute('class', options.classes.icons.restore); + buttonRestore.appendChild(iconRestore); + var iconClose = document.createElement('i'); + iconClose.setAttribute('class', options.classes.icons.close); + buttonClose.appendChild(iconClose); + var size = options.win.getSize() + var position = options.win.getPosition() + var initialPosX = position[0], initialPosY = position[1] + var initialSizeW = size[0], initialSizeH = size[1]; + if (options.customFrameState === 'maximized') { + buttonMaximize.setAttribute('style', buttonMaximize.getAttribute('style') === null ? 'display: none;' : buttonMaximize.getAttribute('style') + 'display: none;'); + options.win.maximize(); + } else if (options.customFrameState === 'fullscreen') { + (options.win.enterFullscreen || options.win.setFullScreen)(true); + } else { + buttonRestore.setAttribute('style', buttonRestore.getAttribute('style') === null ? 'display: none;' : buttonRestore.getAttribute('style') + 'display: none;'); + } + options.win.removeAllListeners('restore'); + options.win.removeAllListeners('minimize'); + options.win.removeAllListeners('maximize'); + options.win.removeAllListeners('enter-fullscreen'); + options.win.removeAllListeners('leave-fullscreen'); + options.win.removeAllListeners('close'); + const onRestore = function () { + console.error('RESTORED') + that.window.localStorage.customFrameState = 'restored'; + buttonRestore.setAttribute('style', buttonRestore.getAttribute('style') === null ? 'display: none;' : buttonRestore.getAttribute('style') + 'display: none;'); + buttonMaximize.setAttribute('style', buttonMaximize.getAttribute('style').replace('display: none;', '')); + mainContainer.setAttribute('style', mainContainer.getAttribute('style').replace('display: none;', '')); + } + that.window.addEventListener('resize', () => { + // Electron doesn't trigger 'restore' event in some cases + if (that.window.localStorage.customFrameState == 'maximized' && !options.win.isMaximized()) { + onRestore() + } + }) + options.win.on('maximize', function () { + console.error('MAXIMIZED') + that.window.localStorage.customFrameState = 'maximized'; + if (buttonMaximize.getAttribute('style') === null || + (buttonMaximize.getAttribute('style') !== null && buttonMaximize.getAttribute('style').indexOf('display: none;') === -1)) { + buttonMaximize.setAttribute('style', buttonMaximize.getAttribute('style') === null ? 'display: none;' : buttonMaximize.getAttribute('style') + 'display: none;'); + } + buttonRestore.setAttribute('style', buttonRestore.getAttribute('style').replace('display: none;', '')); + that.window.localStorage.customFramePosX = initialPosX; + that.window.localStorage.customFramePosY = initialPosY; + that.window.localStorage.customFrameSizeW = initialSizeW; + that.window.localStorage.customFrameSizeH = initialSizeH; + }); + var stateBeforeFullScreen; + options.win.on('enter-fullscreen', function () { + stateBeforeFullScreen = that.window.localStorage.customFrameState; + that.window.localStorage.customFrameState = 'fullscreen'; + mainContainer.setAttribute('style', mainContainer.getAttribute('style') === null ? 'display: none;' : mainContainer.getAttribute('style') + 'display: none;'); + }); + options.win.on('leave-fullscreen', function () { + that.window.localStorage.customFrameState = stateBeforeFullScreen === 'maximized' ? stateBeforeFullScreen : 'restored'; + mainContainer.setAttribute('style', mainContainer.getAttribute('style').replace('display: none;', '')); + }); + options.win.on('restore', onRestore); + options.win.on('minimize', function () { + that.window.localStorage.customFrameState = 'minimized'; + }); + options.win.on('close', function () { + if (that.window.localStorage.customFrameState !== 'maximized') { + const position = options.win.getPosition(), size = options.win.getSize() + that.window.localStorage.customFramePosX = position[0]; + that.window.localStorage.customFramePosY = position[1]; + that.window.localStorage.customFrameSizeW = size[0]; + that.window.localStorage.customFrameSizeH = size[1]; + } + options.win.removeAllListeners('restore'); + options.win.removeAllListeners('minimize'); + options.win.removeAllListeners('maximize'); + options.win.removeAllListeners('enter-fullscreen'); + options.win.removeAllListeners('leave-fullscreen'); + options.win.close(true); + }); + buttonMinimize.addEventListener('click', function () { + options.win.minimize(); + }, {passive: true}); + buttonMaximize.addEventListener('click', function () { + const position = options.win.getPosition() + initialPosX = position[0]; + initialPosY = position[1]; + initialSizeW = size[0]; + initialSizeH = size[1]; + options.win.maximize(); + }, {passive: true}); + buttonRestore.addEventListener('click', function () { + console.error('RESTORING') + options.win.restore(); + }, {passive: true}); + buttonClose.addEventListener('click', function () { + options.win.close(); + }, {passive: true}); + + this.createElement('link', { href: options.style, rel: 'stylesheet', type: 'text/css' }, null, that.document.head); + this.createElement('link', { href: options.uiIconsTheme, rel: 'stylesheet', type: 'text/css' }, null, that.document.head); + this.createElement('link', { href: options.frameTheme, rel: 'stylesheet', type: 'text/css' }, null, that.document.head); + + var body = that.document.body; + buttonsContainer.style.height = mainContainer.style.height; + buttonsContainer.style.lineHeight = mainContainer.style.height; + buttonMinimize.style.height = mainContainer.style.height; + buttonMinimize.style.lineHeight = mainContainer.style.height; + buttonMaximize.style.height = mainContainer.style.height; + buttonMaximize.style.lineHeight = mainContainer.style.height; + buttonRestore.style.height = mainContainer.style.height; + buttonRestore.style.lineHeight = mainContainer.style.height; + buttonClose.style.height = mainContainer.style.height; + buttonClose.style.lineHeight = mainContainer.style.height; + body.insertBefore(mainContainer, body.firstChild); + mainContainer.style.top = 0; + mainContainer.style.left = 0; + body.style.marginTop = mainContainer.offsetHeight + 'px'; + } +} +CustomFrame.attach = (_window, options) => { + const cf = new CustomFrame(_window, options) + cf.create() +} diff --git a/assets/custom-frame/icons/LICENSE.txt b/assets/custom-frame/icons/LICENSE.txt new file mode 100644 index 0000000..8fa3da3 --- /dev/null +++ b/assets/custom-frame/icons/LICENSE.txt @@ -0,0 +1,12 @@ +Font license info + + +## Font Awesome + + Copyright (C) 2016 by Dave Gandy + + Author: Dave Gandy + License: SIL () + Homepage: http://fortawesome.github.com/Font-Awesome/ + + diff --git a/assets/custom-frame/icons/README.txt b/assets/custom-frame/icons/README.txt new file mode 100644 index 0000000..beaab33 --- /dev/null +++ b/assets/custom-frame/icons/README.txt @@ -0,0 +1,75 @@ +This webfont is generated by http://fontello.com open source project. + + +================================================================================ +Please, note, that you should obey original font licenses, used to make this +webfont pack. Details available in LICENSE.txt file. + +- Usually, it's enough to publish content of LICENSE.txt file somewhere on your + site in "About" section. + +- If your project is open-source, usually, it will be ok to make LICENSE.txt + file publicly available in your repository. + +- Fonts, used in Fontello, don't require a clickable link on your site. + But any kind of additional authors crediting is welcome. +================================================================================ + + +Comments on archive content +--------------------------- + +- /font/* - fonts in different formats + +- /css/* - different kinds of css, for all situations. Should be ok with + twitter bootstrap. Also, you can skip style and assign icon classes + directly to text elements, if you don't mind about IE7. + +- demo.html - demo file, to show your webfont content + +- LICENSE.txt - license info about source fonts, used to build your one. + +- config.json - keeps your settings. You can import it back into fontello + anytime, to continue your work + + +Why so many CSS files ? +----------------------- + +Because we like to fit all your needs :) + +- basic file, .css - is usually enough, it contains @font-face + and character code definitions + +- *-ie7.css - if you need IE7 support, but still don't wish to put char codes + directly into html + +- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face + rules, but still wish to benefit from css generation. That can be very + convenient for automated asset build systems. When you need to update font - + no need to manually edit files, just override old version with archive + content. See fontello source code for examples. + +- *-embedded.css - basic css file, but with embedded WOFF font, to avoid + CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. + We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` + server headers. But if you ok with dirty hack - this file is for you. Note, + that data url moved to separate @font-face to avoid problems with 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 B^;G}> literal 0 HcmV?d00001 diff --git a/assets/custom-frame/icons/font/cf-fa.svg b/assets/custom-frame/icons/font/cf-fa.svg new file mode 100644 index 0000000..2b3a8b8 --- /dev/null +++ b/assets/custom-frame/icons/font/cf-fa.svg @@ -0,0 +1,18 @@ + + + +Copyright (C) 2017 by original authors @ fontello.com + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/custom-frame/icons/font/cf-fa.ttf b/assets/custom-frame/icons/font/cf-fa.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c60aa98b05c7dfbac82d956eeceb8983b856fd7b GIT binary patch literal 5420 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 literal 0 HcmV?d00001 diff --git a/assets/scripts/events.js b/assets/scripts/events.js new file mode 100644 index 0000000..c2bd28f --- /dev/null +++ b/assets/scripts/events.js @@ -0,0 +1,41 @@ + +class EventEmitter { + constructor() { + this.events = {}; + } + on(event, listener) { + if (typeof this.events[event] !== 'object') { + this.events[event] = []; + } + this.events[event].push(listener); + return () => this.removeListener(event, listener); + } + removeListener(event, listener) { + if (typeof this.events[event] === 'object') { + const idx = this.events[event].indexOf(listener); + if (idx > -1) { + this.events[event].splice(idx, 1); + } + } + } + removeAllListener(event) { + delete this.events[event] + } + listenerCount(event) { + return this.events[event].length + } + listeners(event) { + return this.events[event] ? this.events[event].slice(0) : [] + } + emit(event, ...args) { + if (typeof this.events[event] === 'object') { + this.events[event].forEach(listener => listener.apply(this, args)) + } + } + once(event, listener) { + const remove = this.on(event, (...args) => { + remove(); + listener.apply(this, args); + }); + } +} \ No newline at end of file diff --git a/bin-debug.js b/bin-debug.js new file mode 100644 index 0000000..c4fd5c4 --- /dev/null +++ b/bin-debug.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const path = require('path'), fs = require('fs'); + +async function findElectronExecutable() { + const relativePaths = [ + 'node_modules/electron/dist/electron', + 'www/nodejs-project/node_modules/electron/dist/electron' + ] + for (const relativePath of relativePaths) { + const fullPath = path.resolve(__dirname, relativePath); + const executable = process.platform === 'win32' ? `${fullPath}.exe` : fullPath; + try { + await fs.promises.access(executable, fs.constants.F_OK); + return executable; + } catch (error) { } + } + + // Check environment variable + const environmentPath = process.env.ELECTRON_PATH; + if (environmentPath) { + try { + await fs.promises.access(environmentPath, fs.constants.F_OK); + return environmentPath; + } catch (error) { + // File not found + } + } + + // Check global NPM installation directory + const npmGlobalPrefix = process.env.npm_global_prefix; + if (npmGlobalPrefix) { + const globalExecutable = path.resolve(npmGlobalPrefix, 'electron/electron'); + const globalExecutableWithExtension = process.platform === 'win32' ? `${globalExecutable}.exe` : globalExecutable; + try { + await fs.promises.access(globalExecutableWithExtension, fs.constants.F_OK); + return globalExecutableWithExtension; + } catch (error) { + // File not found + } + } + + // Default return if not found + return null; +} + +findElectronExecutable().then(electronPath => { + if (electronPath) { + console.log(electronPath) + const child = spawn(electronPath, [ + '--inspect', + '--enable-logging=stderr', + '--trace-warnings', + '--remote-debugging-port=9222', + path.join(__dirname, 'main.js') + ]); + child.stdout.on('data', (data) => { + process.stdout.write(data); + }); + child.stderr.on('data', (data) => { + process.stderr.write(data); + }); + child.on('error', (error) => { + console.error(error); + }); + child.once('close', (code) => { + console.log('exitcode: '+ code) + process.exit(code); + }); + console.log(electronPath) + } else { + console.error('Electron executable not found. Use \'npm i electron@9.1.2\' to install it.') + process.exit(0); + } +}).catch(console.error); diff --git a/bin.js b/bin.js new file mode 100644 index 0000000..8d4678a --- /dev/null +++ b/bin.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const path = require('path'), fs = require('fs'); + +async function findElectronExecutable() { + const relativePaths = [ + 'node_modules/electron/dist/electron', + 'www/nodejs-project/node_modules/electron/dist/electron' + ] + for (const relativePath of relativePaths) { + const fullPath = path.resolve(__dirname, relativePath); + const executable = process.platform === 'win32' ? `${fullPath}.exe` : fullPath; + try { + await fs.promises.access(executable, fs.constants.F_OK); + return executable; + } catch (error) { } + } + + // Check environment variable + const environmentPath = process.env.ELECTRON_PATH; + if (environmentPath) { + try { + await fs.promises.access(environmentPath, fs.constants.F_OK); + return environmentPath; + } catch (error) { + // File not found + } + } + + // Check global NPM installation directory + const npmGlobalPrefix = process.env.npm_global_prefix; + if (npmGlobalPrefix) { + const globalExecutable = path.resolve(npmGlobalPrefix, 'electron/electron'); + const globalExecutableWithExtension = process.platform === 'win32' ? `${globalExecutable}.exe` : globalExecutable; + try { + await fs.promises.access(globalExecutableWithExtension, fs.constants.F_OK); + return globalExecutableWithExtension; + } catch (error) { + // File not found + } + } + + // Default return if not found + return null; +} + +findElectronExecutable().then(electronPath => { + if(electronPath){ + const child = spawn(electronPath, [path.join(__dirname, 'main.js')], { + detached: true, + stdio: 'ignore', + }); + child.unref(); + } else { + console.error('Electron executable not found. Use \'npm i electron@9.1.2\' to install it.') + } + process.exit(0); +}).catch(console.error); \ No newline at end of file diff --git a/package.json b/package.json index a616cde..2f3c8d3 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,14 @@ { "name": "vimer", - "version": "1.0.2", + "version": "1.0.3", "description": "Vimer is an AI powered app with a hassle-free interface for adjusting audio and video.", "main": "main.js", - "scripts": { - "start": "electron ." - }, "window": { "title": "Vimer" }, "author": "Edenware", "license": "MPL-2.0", - "devDependencies": { - "electron": "^9.1.2" - }, + "devDependencies": {}, "repository": { "type": "git", "url": "git+https://github.com/EdenwareApps/Vimer.git" @@ -28,13 +23,24 @@ }, "homepage": "https://github.com/EdenwareApps/Vimer#readme", "dependencies": { + "@electron/remote": "^2.1.2", "@fortawesome/fontawesome-free": "^6.4.2", "adm-zip": "github:efoxbr/adm-zip", "axios": "^1.6.3", + "electron": "^28.2.0", "env-paths": "^2.2.1", "formidable": "^2.1.1", "jquery": "^3.7.1", "openai": "^4.19.1", "sanitize-filename": "^1.6.3" + }, + "bin": { + "megacubo": "./bin.js", + "megacubo-debug": "./bin-debug.js" + }, + "scripts": { + "start": "electron .", + "debug": "electron --inspect --enable-logging=stderr --trace-warnings --remote-debugging-port=9222 .", + "lint": "echo \"No linting configured\"" } } diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..adea12f --- /dev/null +++ b/preload.js @@ -0,0 +1,329 @@ +function getElectron() { + const ret = {}, keys = ['contextBridge', 'ipcRenderer', 'getGlobal', 'screen', 'app', 'shell', 'Tray', 'Menu'] + const extract = electron => { + keys.forEach(k => { + if (electron[k]) ret[k] = electron[k] + }) + } + const electron = require('electron') + extract(electron) + if (electron.remote) { + extract(electron.remote) + } else { + try { + const remote = require('@electron/remote') + extract(remote) + } catch (e) { } + } + keys.forEach(k => { + if (!ret[k]) ret[k] = null + }) + return ret +} + +const { contextBridge, ipcRenderer, getGlobal, screen, app, shell, Tray, Menu } = getElectron() +const Events = require('events'), path = require('path'), fs = require('fs') +const paths = getGlobal('paths') +const { spawn } = require('child_process') + +function download(opts) { + let _reject + const dl = new Download(opts) + const promise = new Promise((resolve, reject) => { + _reject = reject + dl.once('response', statusCode => { + if(statusCode < 200 && statusCode >= 400){ + dl.destroy() + reject('http error '+ statusCode) + } + }) + dl.on('error', e => { + err = e + }) + dl.once('end', buf => { + dl.destroy() + resolve(buf) + }) + if(opts.progress) { + dl.on('progress', opts.progress) + } + dl.start() + }) + promise.cancel = () => { + if(dl && !dl.ended){ + _reject('Promise was cancelled') + dl.destroy() + } + } + return promise +} + +const window = getGlobal('window') + +class FFmpegDownloader { + constructor(){} + async download(target, osd, mask) { + const tmpZipFile = path.join(target, 'ffmpeg.zip') + const arch = process.arch == 'x64' ? 64 : 32 + let osName + switch (process.platform) { + case 'darwin': + osName = 'macos' + break + case 'win32': + osName = 'windows' + break + default: + osName = 'linux' + break + } + const variant = osName + '-' + arch + const url = await this.getVariantURL(variant) + osd.show(mask.replace('{0}', '0%'), 'fas fa-circle-notch fa-spin', 'ffmpeg-dl', 'persistent') + await download({ + url, + file: tmpZipFile, + progress: p => { + osd.show(mask.replace('{0}', p + '%'), 'fas fa-circle-notch fa-spin', 'ffmpeg-dl', 'persistent') + } + }) + const AdmZip = require('adm-zip') + const zip = new AdmZip(tmpZipFile) + const entryName = process.platform == 'win32' ? 'ffmpeg.exe' : 'ffmpeg' + const targetFile = path.join(target, entryName) + zip.extractEntryTo(entryName, target, false, true) + fs.unlink(tmpZipFile, () => {}) + return targetFile + } + async check(osd, mask, folder){ + try { + await fs.promises.access(path.join(this.executableDir, this.executable), fs.constants.F_OK) + return true + } catch (error) { + try { + await fs.promises.access(path.join(folder, this.executable), fs.constants.F_OK) + this.executableDir = folder + return true + } catch (error) { + let err + const file = await this.download(folder, osd, mask).catch(e => err = e) + if (err) { + osd.show(String(err), 'fas fa-exclamation-triangle faclr-red', 'ffmpeg-dl', 'normal') + } else { + osd.show(mask.replace('{0}', '100%'), 'fas fa-circle-notch fa-spin', 'ffmpeg-dl', 'normal') + this.executableDir = path.dirname(file) + this.executable = path.basename(file) + return true + } + } + } + return false + } + async getVariantURL(variant){ + const data = await download({url: 'https://ffbinaries.com/api/v1/versions', responseType: 'json'}) + for(const version of Object.keys(data.versions).sort().reverse()){ + const versionInfo = await download({url: data.versions[version], responseType: 'json'}) + if(versionInfo.bin && typeof(versionInfo.bin[variant]) != 'undefined'){ + return versionInfo.bin[variant].ffmpeg + } + } + } +} + +class FFMpeg extends FFmpegDownloader { + constructor(){ + super() + this.childs = {} + this.executable = 'ffmpeg' + if(process.platform == 'win32'){ + this.executable += '.exe' + } + this.executableDir = process.resourcesPath || path.resolve('ffmpeg') + this.executableDir = this.executableDir.replace(new RegExp('\\\\', 'g'), '/') + if(this.executableDir.indexOf('resources/app') != -1) { + this.executableDir = this.executableDir.split('resources/app').shift() +'resources' + } + this.executable = path.basename(this.executable) + this.tmpdir = paths.temp; + ['exec', 'cleanup', 'check', 'abort'].forEach(k => { + this[k] = this[k].bind(this) // allow export on contextBridge + }) + } + isMetadata(s){ + return s.indexOf('Stream mapping:') != -1 + } + exec(cmd, events){ + let exe, gotMetadata, output = '' + if(process.platform == 'linux' || process.platform == 'darwin'){ // cwd was not being honored on Linux/macOS + exe = this.executableDir +'/'+ this.executable + } else { + exe = this.executable + } + const child = spawn(exe, cmd, { + cwd: this.executableDir, + killSignal: 'SIGINT' + }) + const maxLogLength = 1 * (1024 * 1024), log = s => { + s = String(s) + output += s + if(output.length > maxLogLength){ + output = output.substr(-maxLogLength) + } + if(!gotMetadata && this.isMetadata(s)){ + gotMetadata = true + events.metadata && events.metadata(output) + } + events.data(s) + } + child.stdout.on('data', log) + child.stderr.on('data', log) + child.on('error', err => { + console.log('FFEXEC ERR', cmd, child, err, output) + events.error(err) + }) + child.once('close', () => { + delete this.childs[child.pid] + console.log('FFEXEC DONE', cmd.join(' '), child, output) + events.finish(output) + child.removeAllListeners() + }) + console.log('FFEXEC '+ this.executable, cmd, child) + this.childs[child.pid] = child + events.start && events.start(child.pid) + return child + } + abort(pid){ + if(typeof(this.childs[pid]) != 'undefined'){ + const child = this.childs[pid] + delete this.childs[pid] + child.kill('SIGINT') + } else { + console.log('CANTKILL', pid) + } + } + cleanup(keepIds){ + Object.keys(this.childs).forEach(pid => { + if(keepIds.includes(pid)){ + console.log("Cleanup keeping " + pid) + } else { + console.log("Cleanup kill " + pid) + this.abort(pid) + } + }) + } +} + +class ExternalPlayer { + constructor() { + this.players = [ + {processName: 'vlc', playerName: 'VLC Media Player'}, + {processName: 'smplayer', playerName: 'SMPlayer'}, + {processName: 'mpv', playerName: 'MPV'}, + {processName: 'mplayer', playerName: 'MPlayer'}, + {processName: 'xine', playerName: 'Xine'}, + {processName: 'wmplayer', playerName: 'Windows Media Player'}, + {processName: 'mpc-hc64', playerName: 'Media Player Classic - Home Cinema (64-bit)'}, + {processName: 'mpc-hc', playerName: 'Media Player Classic - Home Cinema (32-bit)'}, + {processName: 'mpc-be64', playerName: 'MPC-BE (64-bit)'}, + {processName: 'mpc-be', playerName: 'MPC-BE (32-bit)'}, + {processName: 'GOM', playerName: 'GOM Player'} + ] + this.play = async (url, chosen) => { + const availables = await this.available() + const player = spawn(availables[chosen], [url], {detached: true, stdio: 'ignore'}) + player.unref() + return true + } + this.available = async () => { + const results = {} + if(!this.finder) { + const ExecFinder = require('exec-finder') + this.finder = new ExecFinder({recursion: 3}) + } + const available = await this.finder.find(this.players.map(p => p.processName)) + Object.keys(available).filter(name => available[name].length).forEach(p => { + const name = this.players.filter(r => r.processName == p).shift().playerName + results[name] = available[p].sort((a, b) => a.length - b.length).shift() + }) + return results + } + } +} + +class WindowProxy extends Events { + constructor() { + super() + this.localEmit = super.emit.bind(this) + this.on = super.on.bind(this) + this.main = getGlobal('ui') + this.port = this.main.opts.port + this.removeAllListeners = super.removeAllListeners.bind(this) + this.emit = (...args) => { + this.main.channel.originalEmit(...args) + } + ipcRenderer.on('message', (_, args) => this.localEmit('message', args)); + ['focus', 'blur', 'show', 'hide', 'minimize', 'maximize', 'restore', 'close', 'isMaximized', 'getPosition', 'getSize', 'setSize', 'setAlwaysOnTop', 'setFullScreen', 'setPosition'].forEach(k => { + this[k] = (...args) => window[k](...args) + }); + ['maximize', 'enter-fullscreen', 'leave-fullscreen', 'restore', 'minimize', 'close'].forEach(k => { + window.on(k, (...args) => this.localEmit(k, ...args)) + }) + } +} + +const windowProxy = new WindowProxy() +const externalPlayer = new ExternalPlayer() +const ffmpeg = new FFMpeg() +const screenScaleFactor = screen.getPrimaryDisplay().scaleFactor || 1 +const getScreen = () => { + const primaryDisplay = screen.getPrimaryDisplay() + const scaleFactor = primaryDisplay.scaleFactor + const bounds = primaryDisplay.bounds + const workArea = primaryDisplay.workArea + const screenData = { + width: bounds.width, + height: bounds.height, + availWidth: workArea.width, + availHeight: workArea.height, + screenScaleFactor: scaleFactor + } + return screenData +} +const restart = () => { + setTimeout(() => { + app.relaunch() + app.quit() + setTimeout(() => app.exit(), 2000) // some deadline + }, 0) +} + +if (parseFloat(process.versions.electron) < 22) { + api = { + platform: process.platform, + window: windowProxy, + openExternal: f => shell.openExternal(f), + openPath: f => shell.openPath(f), + screenScaleFactor, externalPlayer, getScreen, + download, restart, ffmpeg, paths + } +} else { + // On older Electron version (9.1.1) exposing 'require' doesn't works as expected. + contextBridge.exposeInMainWorld( + 'api', { + platform: process.platform, + openExternal: f => shell.openExternal(f), + openPath: f => shell.openPath(f), + window: windowProxy, + screenScaleFactor, + externalPlayer: { + play: externalPlayer.play, + setContext: externalPlayer.setContext + }, + getScreen, + download, + restart, + ffmpeg, + paths + } + ) +}