first commit

This commit is contained in:
René Preuß
2023-03-01 19:24:20 +01:00
commit 4ab5e76d85
45 changed files with 1963 additions and 0 deletions

11
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: 🐞 Bug report
about: Create a report to help us improve
title: "[Bug] the title of bug report"
labels: bug
assignees: ''
---
#### Describe the bug

10
.github/ISSUE_TEMPLATE/help_wanted.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: 🥺 Help wanted
about: Confuse about the use of electron-vue-vite
title: "[Help] the title of help wanted report"
labels: help wanted
assignees: ''
---
#### Describe the problem you confuse

12
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,12 @@
<!-- Thank you for contributing! -->
### Description
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->
- [ ] Bug fix
- [ ] New Feature
- [ ] Documentation update
- [ ] Other

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"

47
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Build
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "**.spec.js"
- ".idea"
- ".vscode"
- ".dockerignore"
- "Dockerfile"
- ".gitignore"
- ".github/**"
- "!.github/workflows/build.yml"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Dependencies
run: npm install
- name: Build Release Files
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: release_on_${{ matrix. os }}
path: release/
retention-days: 5

81
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: CI
on:
pull_request_target:
branches:
- main
permissions:
pull-requests: write
jobs:
job1:
name: Check Not Allowed File Changes
runs-on: ubuntu-latest
outputs:
markdown_change: ${{ steps.filter_markdown.outputs.change }}
markdown_files: ${{ steps.filter_markdown.outputs.change_files }}
steps:
- name: Check Not Allowed File Changes
uses: dorny/paths-filter@v2
id: filter_not_allowed
with:
list-files: json
filters: |
change:
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
# ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml
- name: Comment About Changes We Can't Accept
if: ${{ steps.filter_not_allowed.outputs.change == 'true' }}
uses: actions/github-script@v6
with:
script: |
let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions."
try {
const badFilesArr = [
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
]
const badFiles = badFilesArr.join('\n- ')
const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main <file name>\`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192`
createdComment = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.number,
body: reviewMessage,
})
workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.`
} catch(err) {
console.log("Error creating comment.", err)
}
core.setFailed(workflowFailMessage)
- name: Check Not Linted Markdown
if: ${{ always() }}
uses: dorny/paths-filter@v2
id: filter_markdown
with:
list-files: shell
filters: |
change:
- added|modified: '*.md'
job2:
name: Lint Markdown
runs-on: ubuntu-latest
needs: job1
if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Lint markdown
run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dist-electron
release
out
*.local
# Editor directories and files
.vscode/.debug.env
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# lockfile
package-lock.json
pnpm-lock.yaml
yarn.lock

23
.vscode/.debug.script.mjs vendored Normal file
View File

@@ -0,0 +1,23 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
const pkg = createRequire(import.meta.url)('../package.json')
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// write .debug.env
const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`)
fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n'))
// bootstrap
spawn(
// TODO: terminate `npm run dev` when Debug exits.
process.platform === 'win32' ? 'npm.cmd' : 'npm',
['run', 'dev'],
{
stdio: 'inherit',
env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }),
},
)

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin"
]
}

53
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,53 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"compounds": [
{
"name": "Debug App",
"preLaunchTask": "Before Debug",
"configurations": [
"Debug Main Process",
"Debug Renderer Process"
],
"presentation": {
"hidden": false,
"group": "",
"order": 1
},
"stopAll": true
}
],
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"runtimeArgs": [
"--remote-debugging-port=9229",
"."
],
"envFile": "${workspaceFolder}/.vscode/.debug.env",
"console": "integratedTerminal"
},
{
"name": "Debug Renderer Process",
"port": 9229,
"request": "attach",
"type": "chrome",
"timeout": 60000,
"skipFiles": [
"<node_internals>/**",
"${workspaceRoot}/node_modules/**",
"${workspaceRoot}/dist-electron/**",
// Skip files in host(VITE_DEV_SERVER_URL)
"http://127.0.0.1:3344/**"
]
},
]
}

13
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsc.autoDetect": "off",
"json.schemas": [
{
"fileMatch": [
"/*electron-builder.json5",
"/*electron-builder.json"
],
"url": "https://json.schemastore.org/electron-builder"
}
]
}

31
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,31 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Before Debug",
"type": "shell",
"command": "node .vscode/.debug.script.mjs",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"fileLocation": "relative",
"pattern": {
// TODO: correct "regexp"
"regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$",
"file": 1,
"line": 3,
"column": 4,
"code": 5,
"message": 6
},
"background": {
"activeOnStart": true,
"beginsPattern": "^.*VITE v.* ready in \\d* ms.*$",
"endsPattern": "^.*\\[startup\\] Electron App.*$"
}
}
}
]
}

33
CHANGELOG.md Normal file
View File

@@ -0,0 +1,33 @@
## 2022-10-03
[v2.1.0](https://github.com/electron-vite/electron-vite-vue/pull/267)
- `vite-electron-plugin` is Fast, and WYSIWYG. 🌱
- last-commit: db2e830 v2.1.0: use `vite-electron-plugin` instead `vite-plugin-electron`
## 2022-06-04
[v2.0.0](https://github.com/electron-vite/electron-vite-vue/pull/156)
- 🖖 Based on the `vue-ts` template created by `npm create vite`, integrate `vite-plugin-electron`
- ⚡️ More simplify, is in line with Vite project structure
- last-commit: a15028a (HEAD -> main) feat: hoist `process.env`
## 2022-01-30
[v1.0.0](https://github.com/electron-vite/electron-vite-vue/releases/tag/v1.0.0)
- ⚡️ Main、Renderer、preload, all built with vite
## 2022-01-27
- Refactor the scripts part.
- Remove `configs` directory.
## 2021-11-11
- Refactor the project. Use vite.config.ts build `Main-process`, `Preload-script` and `Renderer-process` alternative rollup.
- Scenic `Vue>=3.2.13`, `@vue/compiler-sfc` is no longer necessary.
- If you prefer Rollup, Use rollup branch.
```bash
Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.
```

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 草鞋没号
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

46
README.md Normal file
View File

@@ -0,0 +1,46 @@
# Rerun Encoder for Rerun Manager
![](https://cdn.discordapp.com/attachments/1041779193979092992/1080483735264301156/NVIDIA_Share_tHZrdoEVPu.gif)
## Introduction
The Rerun Encoder is a tool that allows streamers to transcode videos on their own PCs, eliminating the need to process
videos on the rerunmanager.com servers. This is extremely useful for streamers who have a lot of videos to process, or
streamers who want to process videos as soon as they are done streaming.
## Features
- Transcode videos on your own PC
- Upload videos directly to rerunmanager.com
- Videos up to 12 hours long
## Installation
### Windows
> If Windows Defender SmartScreen blocks the installation, just click "Run anyway". This is because of a missing code
> signing certificate, which we still need to obtain.
1. Download the latest release from the [releases page](https://github.com/bitinflow/rerun-encoder/releases/latest).
2. Once downloaded, execute the installer and follow the instructions.
3. Once the installation is complete, you need to log in with your rerunmanager.com account. You can do this by
clicking the "Login with Rerun Manager" button in the bottom right corner of the application.
## Frequently Asked Questions
### Can I upload more than one video?
No. Right now, the Rerun Encoder only supports uploading one video at a time.
### How many videos can I upload?
Up to 100 videos, currently limited to 10 GB in total, and 12 hours per video (public beta).
### My upload failed, what now?
Uploads may fail if they are not uploaded within two hours. Also, uploads are rejected if they are either too large or
if too many videos have already been uploaded. If you encounter this issue, please contact us on Discord.
### How do I uninstall Rerun Encoder?
Select Start > Settings > Apps > Apps & features. Find the app you want to remove, select More > Uninstall.

11
electron/electron-env.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
VSCODE_DEBUG?: 'true'
DIST_ELECTRON: string
DIST: string
/** /dist/ or /public/ */
PUBLIC: string
}
}

8
electron/main/helpers.ts Normal file
View File

@@ -0,0 +1,8 @@
import {BrowserWindow} from "electron";
export function emit(event: any, ...args: any) {
// Send a message to all windows
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, ...args)
});
}

171
electron/main/index.ts Normal file
View File

@@ -0,0 +1,171 @@
import {app, BrowserWindow, ipcMain, shell} from 'electron'
import {release} from 'node:os'
import {join} from 'node:path'
import {Settings} from "../../shared/schema";
import {SettingsRepository} from "../rerun-manager/settings-repository";
import {Encoder, EncoderOptions} from "../rerun-manager/encoder";
import IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
import {InternalServer} from "../rerun-manager/internal-server";
import {emit} from "./helpers";
import {platform} from "node:process";
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.DIST_ELECTRON = join(__dirname, '..')
process.env.DIST = join(process.env.DIST_ELECTRON, '../dist')
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, '../public')
: process.env.DIST
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit()
}
require('update-electron-app')()
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
let win: BrowserWindow | null = null
// Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html')
async function createWindow(settings: Settings) {
win = new BrowserWindow({
title: 'Rerun Manager - Encoder',
icon: join(process.env.PUBLIC, 'logo.ico'),
frame: false,
maximizable: false,
resizable: false,
width: 560,
height: 300,
backgroundColor: '#0c0c0e',
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false,
},
})
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
win.loadURL(url)
// Open devTool if the app is not packaged
// win.webContents.openDevTools()
} else {
win.loadFile(indexHtml)
}
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString())
})
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith('https:')) shell.openExternal(url)
return {action: 'deny'}
})
// win.webContents.on('will-navigate', (event, url) => { }) #344
}
const settingsRepository = new SettingsRepository();
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
settingsRepository.restore().then((settings: Settings) => {
createWindow(settings)
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (platform !== 'darwin') {
app.quit()
}
})
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
})
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(settingsRepository.getSettings())
}
})
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
})
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`)
} else {
childWindow.loadFile(indexHtml, {hash: arg})
}
})
ipcMain.handle('version', async () => app.getVersion())
ipcMain.handle('settings', async () => settingsRepository.getSettings())
ipcMain.handle('logout', async () => settingsRepository.logout())
ipcMain.handle('quit', async () => app.quit())
ipcMain.handle('minimize', async () => win.minimize())
ipcMain.handle('encode', async (event: IpcMainInvokeEvent, ...args: any[]) => {
const arg = args[0] as { id: string, input: string, options: EncoderOptions };
const encoder = new Encoder(arg.id, arg.input, arg.options, {
onStart: (id) => event.sender.send('encode-start', id),
onProgress: (id, progress) => event.sender.send('encode-progress', id, progress),
onUploadProgress: (id, progress) => event.sender.send('encode-upload-progress', id, progress),
onUploadComplete: (id, video) => event.sender.send('encode-upload-complete', id, video),
onError: (id, error) => event.sender.send('encode-error', id, error),
}, settingsRepository.getSettings());
return await encoder.encode()
})
ipcMain.handle('commitSettings', async (event: IpcMainInvokeEvent, ...args: any[]) => settingsRepository.commitSettings(args[0]))
settingsRepository.watch((settings: Settings) => emit('settings', settings));
const internalServer = new InternalServer();
internalServer.listen(settingsRepository);

176
electron/preload/index.ts Normal file
View File

@@ -0,0 +1,176 @@
import {EncoderOptions} from "../rerun-manager/encoder";
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
return new Promise((resolve) => {
if (condition.includes(document.readyState)) {
resolve(true)
} else {
document.addEventListener('readystatechange', () => {
if (condition.includes(document.readyState)) {
resolve(true)
}
})
}
})
}
const safeDOM = {
append(parent: HTMLElement, child: HTMLElement) {
if (!Array.from(parent.children).find(e => e === child)) {
return parent.appendChild(child)
}
},
remove(parent: HTMLElement, child: HTMLElement) {
if (Array.from(parent.children).find(e => e === child)) {
return parent.removeChild(child)
}
},
}
/**
* https://tobiasahlin.com/spinkit
* https://connoratherton.com/loaders
* https://projects.lukehaas.me/css-loaders
* https://matejkustec.github.io/SpinThatShit
*/
function useLoading() {
const className = `loaders-css__square-spin`
const styleContent = `
@keyframes square-spin {
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
}
.${className} > div {
animation-fill-mode: both;
width: 50px;
height: 50px;
background: #fff;
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
}
.app-loading-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0c0c0e;
z-index: 9;
}
`
const oStyle = document.createElement('style')
const oDiv = document.createElement('div')
oStyle.id = 'app-loading-style'
oStyle.innerHTML = styleContent
oDiv.className = 'app-loading-wrap'
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
return {
appendLoading() {
safeDOM.append(document.head, oStyle)
safeDOM.append(document.body, oDiv)
},
removeLoading() {
safeDOM.remove(document.head, oStyle)
safeDOM.remove(document.body, oDiv)
},
}
}
// ----------------------------------------------------------------------
const {appendLoading, removeLoading} = useLoading()
domReady().then(appendLoading)
window.onmessage = (ev) => {
ev.data.payload === 'removeLoading' && removeLoading()
}
setTimeout(removeLoading, 4999)
// ----------------------------------------------------------------------
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
// boilerplate code for electron...
import {Settings} from "../../shared/schema";
const {
contextBridge,
ipcRenderer
} = require('electron')
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector: string, text: string) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
// end boilerplate code, on to your stuff..
window.api = {
invoke: (channel: string, data: any) => {
let validChannels = [
'encode-start',
'encode-progress',
'encode-upload-progress',
'encode-upload-complete',
'encode-error',
] // list of ipcMain.handle channels you want access in frontend to
if (validChannels.includes(channel)) {
// ipcRenderer.invoke accesses ipcMain.handle channels like 'myfunc'
// make sure to include this return statement or you won't get your Promise back
return ipcRenderer.invoke(channel, data)
}
},
on(channel: string, func: any) {
let validChannels = [
'encode-start',
'encode-progress',
'encode-upload-progress',
'encode-upload-complete',
'encode-error',
] // list of ipcMain.on channels you want access in frontend to
if (validChannels.includes(channel)) {
// ipcRenderer.on accesses ipcMain.on channels like 'myevent'
// make sure to include this return statement or you won't get your Promise back
return ipcRenderer.on(channel, func)
}
},
minimize() {
return ipcRenderer.invoke('minimize')
},
quit() {
return ipcRenderer.invoke('quit')
},
getVersion() {
return ipcRenderer.invoke('version')
},
logout: () => {
return ipcRenderer.invoke('logout')
},
encode: (id: string, input: string, options: EncoderOptions) => {
return ipcRenderer.invoke('encode', {id, input, options})
},
commitSettings: (settings: Settings) => {
return ipcRenderer.invoke('commitSettings', settings)
},
getSettings: () => {
return ipcRenderer.invoke('settings')
},
onSettingsChanged: (callback: (settings: Settings) => void) => {
return ipcRenderer.on('settings', (event: any, ...args: any) => callback(args[0]))
},
}

View File

@@ -0,0 +1,147 @@
import {EncoderListeners, EncoderOptions, Settings, User, Video} from "../../shared/schema";
import * as fs from "fs";
import axios, {AxiosInstance} from "axios";
const ffmpeg = require('fluent-ffmpeg')
export class Encoder {
private readonly id: string;
private readonly input: string;
private readonly output: string;
private readonly options: EncoderOptions;
private readonly listeners: EncoderListeners;
private readonly settings: Settings;
private api: AxiosInstance;
private s3: AxiosInstance;
constructor(
id: string,
input: string,
options: EncoderOptions,
listeners: EncoderListeners,
settings: Settings
) {
this.id = id;
this.input = input;
this.output = this.input.replace(/\.mp4$/, '.flv')
this.options = options;
this.listeners = listeners;
this.settings = settings;
this.api = axios.create({
baseURL: settings.endpoint,
headers: {
Authorization: `${settings.credentials.token_type} ${settings.credentials.access_token}`
}
})
this.s3 = axios.create({});
}
async encode(): Promise<void> {
this.listeners.onStart(this.id)
ffmpeg(this.input)
.outputOptions(this.getOutputOptions())
.output(this.output)
.on('start', () => {
console.log('start')
})
.on('progress', (progress) => {
console.log('progress', progress)
this.listeners.onProgress(this.id, progress.percent)
})
.on('end', async () => {
console.log('end')
// @ts-ignore
try {
const video = await this.requestUploadUrl(this.settings.credentials.user)
await this.upload(video)
} catch (error) {
console.log('error', error)
this.listeners.onError(this.id, error.message)
}
})
.on('error', (error) => {
console.log('error', error)
this.listeners.onError(this.id, error.message)
})
.run()
}
private getOutputOptions() {
return [
'-c:v libx264',
'-preset veryfast',
'-crf 23',
'-maxrate 6000k',
'-bufsize 6000k',
'-c:a aac',
'-b:a 128k',
'-ac 2',
'-f flv',
'-movflags +faststart',
'-y'
]
}
private async requestUploadUrl(user: User): Promise<Video> {
const response = await this.api.post(`channels/${user.id}/videos`, {
title: this.options.title,
size: fs.statSync(this.output).size,
})
return response.data as Video
}
private async confirmUpload(video: Video): Promise<Video> {
const response = await this.api.post(`videos/${video.id}/confirm`, {
encoded: true,
})
console.log('confirm', response.data)
return response.data as Video;
}
private async cancelUpload(video: Video): Promise<void> {
await this.api.delete(`videos/${video.id}/cancel`)
}
private async upload(video: Video) {
if (!video.upload_url) {
return
}
const stream = fs.createReadStream(this.output)
stream.on('error', (error) => {
console.log('upload error', error)
this.listeners.onError(this.id, error.message)
})
const progress = (progress: any) => {
const progressCompleted = progress.loaded / fs.statSync(this.output).size * 100
this.listeners.onUploadProgress(this.id, progressCompleted)
}
try {
await this.s3.put(video.upload_url, stream, {
onUploadProgress: progress,
headers: {
'Content-Type': 'video/x-flv',
}
})
this.listeners.onUploadComplete(this.id, await this.confirmUpload(video))
} catch (error) {
console.log('upload error', error)
try {
await this.cancelUpload(video)
} catch (error) {
console.log('cancel error', error)
}
this.listeners.onError(this.id, `Upload Error: ${error.message}`)
}
}
}

View File

@@ -0,0 +1,101 @@
import Application from 'koa';
import Router from 'koa-router';
import KoaLogger from 'koa-logger';
import json from 'koa-json';
import koaCors from "koa-cors";
import bodyParser from "koa-bodyparser";
import axios, {AxiosInstance} from "axios";
import {app} from "electron"
import {Credentials, User} from "../../shared/schema";
import {SettingsRepository} from "./settings-repository";
import * as fs from "fs";
import {join} from "node:path";
export class InternalServer {
private readonly app: Application;
private readonly axios: AxiosInstance;
constructor() {
this.axios = axios.create();
this.app = new Application();
}
listen(settingsRepository: SettingsRepository) {
const router = new Router()
router.get("/health", (ctx) => {
const settings = settingsRepository.getSettings();
ctx.body = {
status: 'ok',
version: app.getVersion(),
has_credentials: settings.credentials !== null,
}
});
router.get('/oauth', (ctx) => {
if(process.env.VITE_DEV_SERVER_URL) {
ctx.body = fs.readFileSync(join(__dirname, '..', '..', 'public', 'callback.html'), {encoding: 'utf8'})
} else {
ctx.body = fs.readFileSync(join(process.env.DIST, 'callback.html'), {encoding: 'utf8'})
}
});
router.post('/callback', async (ctx) => {
// @ts-ignore
const params = new URLSearchParams(ctx.request.body.hash);
const credentials: Credentials = {
access_token: params.get('access_token'),
token_type: params.get('token_type'),
expires_in: params.get('expires_in'),
expires_at: this.calculateExpiresAt(params.get('expires_in')),
state: params.get('state'),
user: await this.resolveUser(params.get('access_token'), params.get('token_type')),
}
console.log('credentials', credentials);
settingsRepository.setCredentials(credentials);
await settingsRepository.save();
ctx.body = {
status: 'ok',
}
});
this.app.use(json());
this.app.use(KoaLogger());
this.app.use(koaCors());
this.app.use(bodyParser());
this.app.use(router.routes());
this.app.listen(8361, () => {
console.log(`Server listening http://127.0.0.1:8361/`);
});
}
private async resolveUser(accessToken: string, tokenType: string): Promise<User> {
const response = await this.axios.get('https://api.rerunmanager.com/v1/channels/me', {
headers: {
'Accept': 'application/json',
'Authorization': `${tokenType} ${accessToken}`,
}
});
return {
id: response.data.id,
name: response.data.name,
config: response.data.config,
avatar_url: response.data.avatar_url,
}
}
private calculateExpiresAt(expiresIn: string) {
const now = new Date();
const expiresAt = new Date(now.getTime() + parseInt(expiresIn) * 1000);
return expiresAt.toISOString();
}
}

View File

@@ -0,0 +1,124 @@
import defu from 'defu'
import {platform} from 'node:process'
import * as fs from 'fs'
import {Credentials, Settings} from "../../shared/schema";
const defaults: Settings = {
version: '1.0.0',
credentials: null,
endpoint: 'https://api.rerunmanager.com/v1/',
}
export class SettingsRepository {
private settings: Settings | null
private readonly path: string
private readonly listeners: Array<(settings: Settings) => void>;
private readonly directory: string;
constructor() {
this.listeners = []
this.settings = defaults
this.directory = this.appdata('/rerunmanager')
if (!fs.existsSync(this.directory)) {
console.log('Creating directory', this.directory)
fs.mkdirSync(this.directory)
}
this.path = `${this.directory}/encoder.json`
}
async restore() {
// load settings from path
// using node's fs module if file does not exist, create it with defaults
// if file exists, load it into this.settings
try {
const data = await fs.promises.readFile(this.path, {encoding: 'utf-8'})
if (data) {
const settings = JSON.parse(data) as Settings;
if (settings.version !== defaults.version) {
console.log('Settings version mismatch, resetting to defaults');
this.settings = defaults
} else {
console.log('Settings version match, merge with defaults');
this.settings = defu.defu(settings, defaults)
}
} else {
console.log('Settings file empty, resetting to defaults')
this.settings = defaults
}
} catch (e) {
console.log('Settings file not found, resetting to defaults', e)
this.settings = defaults
}
// check if settings.credentials.expires_at is in the past
// if so, set settings.credentials to null
if (this.isExpired()) {
console.log('Credentials expired!');
}
await this.save()
return this.settings
}
async save() {
// call all listeners with the current settings
this.listeners.forEach(
(listener: (settings: Settings) => void) => listener(this.settings)
)
const pretty = JSON.stringify(this.settings, null, 2)
await fs.promises.writeFile(this.path, pretty, {encoding: 'utf-8'})
}
appdata(path: string): string {
if (process.env.APPDATA) {
return process.env.APPDATA + path
}
if (platform === 'darwin') {
return process.env.HOME + '/Library/Preferences' + path
}
return process.env.HOME + "/.local/share" + path
}
getSettings() {
return this.settings
}
watch(callback: (settings: Settings) => void) {
// add callback to list of listeners
this.listeners.push(callback)
}
setCredentials(credentials: Credentials) {
this.settings.credentials = credentials
}
commitSettings(settings: Settings) {
this.settings = defu(settings, this.settings)
this.save()
}
async logout() {
this.settings.credentials = null
await this.save()
}
private isExpired() {
if (this.settings.credentials) {
const expiresAt = new Date(this.settings.credentials.expires_at)
const now = new Date()
return expiresAt < now
}
return true;
}
}

47
forge.config.js Normal file
View File

@@ -0,0 +1,47 @@
const { utils: { fromBuildIdentifier } } = require('@electron-forge/core');
module.exports = {
buildIdentifier: process.env.IS_BETA ? 'beta' : 'prod',
packagerConfig: {
appBundleId: fromBuildIdentifier({ beta: 'com.rerunmanager.encoder-beta', prod: 'com.rerunmanager.encoder' }),
icon: 'public/logo'
},
rebuildConfig: {},
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'bitinflow',
name: 'rerun-encoder'
},
prerelease: true
}
}
],
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
iconUrl: 'https://cdn.bitinflow.com/rerunmanager/encoder/logo.ico',
setupIcon: 'public/logo.ico',
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {
options: {
icon: 'public/logo.png'
}
}
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
};

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/x-icon" href="/logo.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' https://cdn.bitinflow.com;"/>
<link rel="stylesheet" href="https://cdn.bitinflow.com/fontawesome/6.2.0/css/all.min.css"
integrity="sha384-a/J8ePf6YlZ5aBQ6xmsPiCGV52z4VJMixEqX+4Mvoxi8flAWQA00ZFtk5qO5vyrP" crossorigin="anonymous">
<title>Rerun Manager - Encoder</title>
</head>
<body class="h-full">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

75
package.json Normal file
View File

@@ -0,0 +1,75 @@
{
"name": "rerun-encoder",
"private": true,
"productName": "Rerun Encoder",
"version": "1.0.0",
"description": "Official Rerun Encoder App for Rerun Manager",
"main": "dist-electron/main/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build && electron-forge make",
"eletron:package": "electron-forge package",
"eletron:make": "electron-forge make",
"eletron:publish": "electron-forge publish",
"preview": "vite preview"
},
"keywords": [],
"author": {
"name": "bitinflow GbR",
"email": "support@bitinflow.com"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bitinflow/rerun-encoder.git"
},
"debug": {
"env": {
"VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/"
}
},
"devDependencies": {
"@electron-forge/cli": "^6.0.4",
"@electron-forge/core": "^6.0.5",
"@electron-forge/maker-deb": "^6.0.4",
"@electron-forge/maker-rpm": "^6.0.4",
"@electron-forge/maker-squirrel": "^6.0.4",
"@electron-forge/maker-zip": "^6.0.4",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@types/cors": "^2.8.13",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/koa-cors": "^0.0.2",
"@types/koa-json": "^2.0.20",
"@types/koa-logger": "^3.1.2",
"@types/koa-router": "^7.4.4",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"electron": "^23.1.1",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.12.1",
"vue": "^3.2.47",
"vue-tsc": "^1.1.7"
},
"dependencies": {
"@koa/cors": "^4.0.0",
"axios": "^1.3.4",
"cors": "^2.8.5",
"defu": "^6.1.2",
"electron-squirrel-startup": "^1.0.0",
"express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"koa": "^2.14.1",
"koa-bodyparser": "^4.3.0",
"koa-cors": "^0.0.16",
"koa-json": "^2.0.2",
"koa-logger": "^3.2.1",
"koa-router": "^12.0.0",
"update-electron-app": "^2.0.1"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

36
public/callback.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<title>Login Successful</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="h-full">
<div class="min-h-full pt-16 pb-12 flex flex-col bg-white">
<main class="flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-16">
<div class="text-center">
<p class="text-sm font-semibold text-purple-600 uppercase tracking-wide">Rerun Encoder</p>
<h1 class="mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">Login Successful</h1>
<p class="mt-2 text-base text-gray-500">You may close the page now.</p>
<div class="mt-6">
<a href="javascript:close()" class="text-base font-medium text-purple-600 hover:text-purple-500">Close window<span aria-hidden="true"> &rarr;</span></a>
</div>
</div>
</div>
</main>
</div>
<script type="application/javascript">
fetch("/callback", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
hash: location.hash.substring(1),
})
}).then(res => {
console.log("Request complete! response:", res);
setTimeout(() => close(), 1000);
});
</script>
</body>
</html>

BIN
public/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

66
shared/schema.d.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
/**
* User structure
*/
export interface User {
id: string
name: string
avatar_url: string
config: {
storage_limit: number
videos_limit: number
storage_used: number
}
}
/**
* Oauth2 credential structure including user information
*/
export interface Credentials {
access_token: string
token_type: string
expires_in: string
state: string
expires_at: string
user: User
}
/**
* File structure for user settings
* Which is stored in %APPDATA%/rerun-manager/encoder.json
*/
export interface Settings {
version?: string // version of the settings schema (diff will force a reset)
credentials?: Credentials | null,
endpoint?: string | null,
}
export interface VerifiedGame {
id: number
name: string
publisher: string | null
notes: string | null
supported: boolean
requires_optimization: boolean
executables: Array<string>
image_url: string | null
}
export interface EncoderOptions {
publishOnComplete: boolean;
title: string
}
export interface EncoderListeners {
onStart: (id) => void;
onProgress: (id, progress: number) => void;
onUploadProgress: (id, progress: number) => void;
onUploadComplete: (id, video: Video) => void;
onError: (id, error: string) => void;
}
export interface Video {
id: string;
upload_url?: string;
title: string;
status: 'created' | 'uploaded'
}

261
src/App.vue Normal file
View File

@@ -0,0 +1,261 @@
<script setup lang="ts">
import {computed, onMounted, ref} from "vue";
import ProgressBar from "./components/ProgressBar.vue";
import AppTitleBar from "./components/AppTitleBar.vue";
import VButton from "./components/VButton.vue";
import {Settings, Video} from "../shared/schema";
console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`)
const isEncoding = ref(false)
const isCompleted = ref(false)
const encodingProgress = ref(0)
const encodingProgressStage = ref<string>('unknown')
const encodingError = ref<string | null>(null)
const version = ref<string>('unknown')
const reset = () => {
encodingProgress.value = 0
encodingProgressStage.value = 'unknown'
encodingError.value = null
isEncoding.value = false
isCompleted.value = false
}
const requestEncode = (event: any) => {
// get file from @change event
const file = event.target.files[0] as File
// @ts-ignore
window.api.on('encode-start', (event: any, id: string) => {
encodingProgressStage.value = 'starting'
encodingProgress.value = 0
console.log('encode-start', id)
})
// @ts-ignore
window.api.on('encode-progress', (event: any, id: string, progress: number) => {
encodingProgress.value = progress
encodingProgressStage.value = 'transcoding'
console.log('encode-progress', id, progress)
})
// @ts-ignore
window.api.on('encode-upload-progress', (event: any, id: string, progress: number) => {
encodingProgress.value = progress
encodingProgressStage.value = 'uploading'
console.log('encode-upload-progress', id, progress)
})
// @ts-ignore
window.api.on('encode-upload-complete', (event: any, id: string, video: Video) => {
encodingProgressStage.value = 'complete'
encodingProgress.value = 100
isEncoding.value = false
isCompleted.value = true
console.log('encode-upload-complete', id, video)
})
// @ts-ignore
window.api.on('encode-error', (event: any, id: string, error: any) => {
encodingProgressStage.value = 'error'
encodingProgress.value = 0
isEncoding.value = false
isCompleted.value = false
encodingError.value = error
console.log('encode-error', id, error)
})
try {
isEncoding.value = true
encodingError.value = null
isCompleted.value = false
// @ts-ignore
window.api.encode('file-upload-123', file.path, {
title: file.name,
publishOnComplete: true,
})
} catch (err) {
isEncoding.value = false
isCompleted.value = false
encodingError.value = 'Failed to encode video'
console.error(err)
}
}
const settings = ref<Settings | null>(null);
const fileInput = ref<HTMLInputElement | null>(null)
// Authorization Code Grant with PKCE
const oauthHref = computed(() => {
const params = new URLSearchParams({
client_id: '97be2e0d-fc64-4a0c-af7b-959f754f516e',
redirect_uri: 'http://localhost:8361/oauth',
response_type: 'token',
scope: 'user:read video:read video:manage',
})
return `https://rerunmanager.com/oauth/authorize?${params.toString()}`
})
const settingsChanged = (settings: any) => {
console.log('settingsChanged', settings)
}
const logout = () => {
// @ts-ignore
window.api.logout()
}
const storageUpgradeRequired = computed(() => {
if (!settings.value) return false
// @ts-ignore
return !settings.value.credentials.user.config.premium
})
onMounted(() => {
// @ts-ignore
window.api.onSettingsChanged((x: any) => settings.value = x)
// @ts-ignore
window.api.getSettings()
.then((x: any) => settings.value = x)
.catch((err: any) => console.error(err))
// @ts-ignore
window.api.getVersion().then((x: any) => version.value = `v${x}`)
})
</script>
<template>
<div class="app h-screen w-screen bg-base-800 text-white flex flex-col justify-between">
<div>
<app-title-bar/>
<div class="px-6 flex">
<div>
<img src="./assets/ffmpeg.svg" class="h-16" alt="ffmpeg">
</div>
<div>
<div class="font-bold text-xl mt-2">Rerun Encoder</div>
<div class="text-sm text-zinc-400">Powered by FFmpeg</div>
<div class="flex gap-4 mt-4">
<a
class="text-rose-400"
href="https://www.bitinflow.com/legal/privacy/"
target="_blank"
>
<i class="far fa-arrow-up-right-from-square"></i>
Privacy & terms
</a>
<a
class="text-rose-400"
href="https://github.com/bitinflow/rerun-encoder#readme"
target="_blank"
>
<i class="far fa-arrow-up-right-from-square"></i>
More details
</a>
</div>
</div>
</div>
</div>
<div>
<div>
<div v-if="settings && settings.credentials" class="p-4 flex justify-between bg-base-900">
<div class="test flex gap-2">
<div>
<img
class="h-16 rounded-full"
:src="settings.credentials.user.avatar_url"
alt="channel avatar"
>
</div>
<div class="self-center">
<div class="text-lg">{{ settings.credentials.user.name }}</div>
<div class="text-sm text-zinc-400">
{{ settings.credentials.user.config.storage_used }}
of {{ settings.credentials.user.config.storage_limit }}
GB used
</div>
<div class="flex text-sm text-zinc-600 gap-1">
<div>
{{ version }}
</div>
<div class="text-zinc-700">|</div>
<a href="#" @click.prevent="logout">Logout</a>
</div>
</div>
</div>
<div class="self-center">
<div v-if="encodingError" class="text-right">
<div class="text-rose-400 text-sm w-64">
{{ encodingError }}
</div>
<a href="#"
class="text-zinc-400 text-sm"
@click="reset">
Retry
</a>
</div>
<div v-else-if="isCompleted" class="text-right">
<div class="text-emerald-400 w-64">
<i class="fa-solid fa-check"></i>
Video uploaded successfully!
</div>
<a href="#"
class="text-zinc-400 text-sm"
@click="reset">
Upload another
</a>
</div>
<template v-else-if="isEncoding">
<progress-bar :progress="encodingProgress" :stage="encodingProgressStage"/>
</template>
<template v-else>
<input
ref="fileInput"
class="hidden"
type="file"
accept="video/mp4"
@change="requestEncode"
>
<v-button
@click="fileInput && fileInput.click()"
:disabled="isEncoding"
>
<i class="fal fa-upload"></i>
Transcode & Upload
</v-button>
</template>
</div>
</div>
<div v-else class="p-4 flex justify-between bg-base-900">
<div class="self-center opacity-70">
Login required
</div>
<div class="py-5">
<a :href="oauthHref" class="bg-primary-500 py-1.5 px-4 rounded text-white" target="_blank">
<i class="fal fa-lock"></i>
Login with Rerun Manager
</a>
</div>
</div>
</div>
<div
v-if="storageUpgradeRequired"
class="bg-primary-500 text-white px-4 py-1 text-sm flex justify-between">
<div>
<i class="fa-solid fa-stars"></i>
Need more storage? Get Rerun Manager Premium!
</div>
<div class="text-sm">
<a href="https://www.rerunmanager.com/plans" target="_blank" class="text-white">
<i class="far fa-arrow-up-right-from-square"></i>
Learn more
</a>
</div>
</div>
</div>
</div>
</template>

1
src/assets/electron.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><g fill="none" fill-rule="evenodd"><circle fill="#2B2E3A" cx="128" cy="128" r="128"/><g fill="#9FEAF9" fill-rule="nonzero"><path d="M100.502 71.69c-26.005-4.736-46.567.221-54.762 14.415-6.115 10.592-4.367 24.635 4.24 39.646a2.667 2.667 0 1 0 4.626-2.653c-7.752-13.522-9.261-25.641-4.247-34.326 6.808-11.791 25.148-16.213 49.187-11.835a2.667 2.667 0 0 0 .956-5.247zm-36.999 72.307c10.515 11.555 24.176 22.394 39.756 31.388 37.723 21.78 77.883 27.601 97.675 14.106a2.667 2.667 0 1 0-3.005-4.406c-17.714 12.078-55.862 6.548-92.003-14.318-15.114-8.726-28.343-19.222-38.478-30.36a2.667 2.667 0 1 0-3.945 3.59z"/><path d="M194.62 140.753c17.028-20.116 22.973-40.348 14.795-54.512-6.017-10.423-18.738-15.926-35.645-16.146a2.667 2.667 0 0 0-.069 5.333c15.205.198 26.165 4.939 31.096 13.48 6.792 11.765 1.49 29.807-14.248 48.399a2.667 2.667 0 1 0 4.071 3.446zm-43.761-68.175c-15.396 3.299-31.784 9.749-47.522 18.835-38.942 22.483-64.345 55.636-60.817 79.675a2.667 2.667 0 1 0 5.277-.775c-3.133-21.344 20.947-52.769 58.207-74.281 15.267-8.815 31.135-15.06 45.972-18.239a2.667 2.667 0 1 0-1.117-5.215z"/><path d="M87.77 187.753c8.904 24.86 23.469 40.167 39.847 40.167 11.945 0 22.996-8.143 31.614-22.478a2.667 2.667 0 1 0-4.571-2.748c-7.745 12.883-17.258 19.892-27.043 19.892-13.605 0-26.596-13.652-34.825-36.63a2.667 2.667 0 1 0-5.021 1.797zm81.322-4.863c4.61-14.728 7.085-31.718 7.085-49.423 0-44.179-15.463-82.263-37.487-92.042a2.667 2.667 0 0 0-2.164 4.874c19.643 8.723 34.317 44.866 34.317 87.168 0 17.177-2.397 33.63-6.84 47.83a2.667 2.667 0 1 0 5.09 1.593zm50.224-2.612c0-7.049-5.714-12.763-12.763-12.763-7.049 0-12.763 5.714-12.763 12.763 0 7.049 5.714 12.763 12.763 12.763 7.049 0 12.763-5.714 12.763-12.763zm-5.333 0a7.43 7.43 0 1 1-14.86 0 7.43 7.43 0 0 1 14.86 0zM48.497 193.041c7.05 0 12.764-5.714 12.764-12.763 0-7.049-5.715-12.763-12.764-12.763-7.048 0-12.763 5.714-12.763 12.763 0 7.049 5.715 12.763 12.763 12.763zm0-5.333a7.43 7.43 0 1 1 0-14.86 7.43 7.43 0 0 1 0 14.86z"/><path d="M127.617 54.444c7.049 0 12.763-5.714 12.763-12.763 0-7.049-5.714-12.763-12.763-12.763-7.049 0-12.763 5.714-12.763 12.763 0 7.049 5.714 12.763 12.763 12.763zm0-5.333a7.43 7.43 0 1 1 0-14.86 7.43 7.43 0 0 1 0 14.86zm1.949 93.382c-4.985 1.077-9.896-2.091-10.975-7.076a9.236 9.236 0 0 1 7.076-10.976c4.985-1.077 9.896 2.091 10.976 7.076 1.077 4.985-2.091 9.897-7.077 10.976z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

4
src/assets/ffmpeg.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path fill="#ffffff"
d="M39.5,42h-9c-1,0-1.9-0.6-2.3-1.5c-0.4-0.9-0.2-2,0.5-2.7l8.3-8.3v-3.9L21.3,41.3 c-0.5,0.5-1.1,0.7-1.8,0.7h-11c-1,0-1.9-0.6-2.3-1.5c-0.4-0.9-0.2-2,0.5-2.7L33.5,11h-3.9L10.3,30.3c-0.7,0.7-1.8,0.9-2.7,0.5 C6.6,30.4,6,29.5,6,28.5v-11c0-0.7,0.3-1.3,0.7-1.8l4.7-4.7h-3C7.1,11,6,9.9,6,8.5S7.1,6,8.5,6h9c1,0,1.9,0.6,2.3,1.5 c0.4,0.9,0.2,2-0.5,2.7L11,18.5v3.9L26.7,6.7C27.2,6.3,27.8,6,28.5,6h11c1,0,1.9,0.6,2.3,1.5c0.4,0.9,0.2,2-0.5,2.7L14.5,37h3.9 l19.3-19.3c0.7-0.7,1.8-0.9,2.7-0.5c0.9,0.4,1.5,1.3,1.5,2.3v11c0,0.7-0.3,1.3-0.7,1.8L36.5,37h3c1.4,0,2.5,1.1,2.5,2.5 S40.9,42,39.5,42z"/>
</svg>

After

Width:  |  Height:  |  Size: 708 B

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,34 @@
<template>
<div class="flex justify-between p-4 text-sm title-bar">
<div class="flex gap-2">
<i class="fas fa-solid fa-arrow-rotate-right self-center"></i>
<div>
Rerun Manager - Encoder
</div>
</div>
<div class="flex gap-3">
<a href="#" @click="minimizeCompanion">
<i class="fal fa-dash"></i>
</a>
<a href="#" @click="closeCompanion">
<i class="fal fa-xmark"></i>
</a>
</div>
</div>
</template>
<script lang="ts">
export default {
name: "AppTitleBar",
methods: {
minimizeCompanion() {
// @ts-ignore
window.api.minimize()
},
closeCompanion() {
// @ts-ignore
window.api.quit();
},
},
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="flex flex-col items-center justify-center gap-1.5 w-64">
<div class="text-xs text-zinc-400 flex justify-between w-full">
<div class="font-bold capitalize">{{ stage }}</div>
<div>{{ format(progress) }}%</div>
</div>
<div class="w-full h-2 bg-base-500 rounded-full">
<div
class="h-2 bg-primary-500 rounded-full"
:style="{ width: `${progress}%` }"
></div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: "ProgressBar",
props: {
progress: {
type: Number,
default: 0
},
stage: {
type: String,
default: ""
}
},
methods: {
format(progress: number) {
return Math.round(progress)
}
}
}
</script>

View File

@@ -0,0 +1,16 @@
<template>
<button
type="button"
@click="$emit('click')"
class="bg-primary-500 py-1.5 px-4 rounded text-white"
>
<slot/>
</button>
</template>
<script lang="ts">
export default {
name: "VButton",
emits: ['click']
}
</script>

10
src/main.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import './samples/node-api'
createApp(App)
.mount('#app')
.$nextTick(() => {
postMessage({ payload: 'removeLoading' }, '*')
})

13
src/samples/node-api.ts Normal file
View File

@@ -0,0 +1,13 @@
import { lstat } from 'node:fs/promises'
import { cwd } from 'node:process'
import { ipcRenderer } from 'electron'
ipcRenderer.on('main-process-message', (_event, ...args) => {
console.log('[Receive Main-process message]:', ...args)
})
lstat(cwd()).then(stats => {
console.log('[fs.lstat]', stats)
}).catch(err => {
console.error(err)
})

12
src/style.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
-webkit-user-select: none;
-webkit-app-region: drag;
}
button, a, input, label {
-webkit-app-region: no-drag;
}

7
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

49
tailwind.config.js Normal file
View File

@@ -0,0 +1,49 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx,vue}",
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#9146FF',
'50': '#FEFEFF',
'100': '#F2E9FF',
'200': '#DAC0FF',
'300': '#C298FF',
'400': '#A96FFF',
'500': '#9146FF',
'600': '#700EFF',
'700': '#5600D5',
'800': '#40009D',
'900': '#290065'
},
base: {
DEFAULT: '#26262C',
'50': '#A8A8B4',
'100': '#9D9DAA',
'200': '#878797',
'300': '#727284',
'400': '#5F5F6E',
'500': '#4C4C58',
'600': '#393942',
'700': '#26262C',
'800': '#0C0C0E',
'900': '#000000'
},
purple: '#8d63f2',
emerald: colors.emerald,
rose: colors.rose,
}
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
}

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "commonjs",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src", "shared"],
"references": [
{ "path": "./tsconfig.node.json" }
]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json", "electron", "shared"]
}

75
vite.config.ts Normal file
View File

@@ -0,0 +1,75 @@
import { rmSync } from 'node:fs'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer'
import pkg from './package.json'
import {resolve} from "path";
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
rmSync('dist-electron', { recursive: true, force: true })
const isServe = command === 'serve'
const isBuild = command === 'build'
const sourcemap = isServe || !!process.env.VSCODE_DEBUG
return {
plugins: [
vue(),
electron([
{
// Main-Process entry file of the Electron App.
entry: 'electron/main/index.ts',
onstart(options) {
if (process.env.VSCODE_DEBUG) {
console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App')
} else {
options.startup()
}
},
vite: {
build: {
sourcemap,
minify: isBuild,
outDir: 'dist-electron/main',
rollupOptions: {
external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
},
},
},
},
{
entry: 'electron/preload/index.ts',
onstart(options) {
// Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete,
// instead of restarting the entire Electron App.
options.reload()
},
vite: {
build: {
sourcemap: sourcemap ? 'inline' : undefined, // #332
minify: isBuild,
outDir: 'dist-electron/preload',
rollupOptions: {
external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
},
},
},
}
]),
// Use Node.js API in the Renderer-process
renderer({
nodeIntegration: true,
}),
],
server: process.env.VSCODE_DEBUG && (() => {
const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
return {
host: url.hostname,
port: +url.port,
}
})(),
clearScreen: false,
}
})