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 + + +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 0000000..c60aa98 Binary files /dev/null and b/assets/custom-frame/icons/font/cf-fa.ttf differ diff --git a/assets/custom-frame/icons/font/cf-fa.woff b/assets/custom-frame/icons/font/cf-fa.woff new file mode 100644 index 0000000..134cbf6 Binary files /dev/null and b/assets/custom-frame/icons/font/cf-fa.woff differ diff --git a/assets/custom-frame/icons/font/cf-fa.woff2 b/assets/custom-frame/icons/font/cf-fa.woff2 new file mode 100644 index 0000000..adaa3a4 Binary files /dev/null and b/assets/custom-frame/icons/font/cf-fa.woff2 differ 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 + } + ) +}