mirror of
https://github.com/bitinflow/rerun-encoder.git
synced 2026-03-13 13:46:00 +00:00
first commit
This commit is contained in:
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
10
.github/ISSUE_TEMPLATE/help_wanted.md
vendored
Normal 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
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
11
.github/dependabot.yml
vendored
Normal 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
47
.github/workflows/build.yml
vendored
Normal 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
81
.github/workflows/ci.yml
vendored
Normal 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
31
.gitignore
vendored
Normal 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
23
.vscode/.debug.script.mjs
vendored
Normal 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
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
||||
53
.vscode/launch.json
vendored
Normal file
53
.vscode/launch.json
vendored
Normal 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
13
.vscode/settings.json
vendored
Normal 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
31
.vscode/tasks.json
vendored
Normal 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
33
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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
46
README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Rerun Encoder for Rerun Manager
|
||||
|
||||

|
||||
|
||||
## 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
11
electron/electron-env.d.ts
vendored
Normal 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
8
electron/main/helpers.ts
Normal 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
171
electron/main/index.ts
Normal 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
176
electron/preload/index.ts
Normal 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]))
|
||||
},
|
||||
}
|
||||
147
electron/rerun-manager/encoder.ts
Normal file
147
electron/rerun-manager/encoder.ts
Normal 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
101
electron/rerun-manager/internal-server.ts
Normal file
101
electron/rerun-manager/internal-server.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
124
electron/rerun-manager/settings-repository.ts
Normal file
124
electron/rerun-manager/settings-repository.ts
Normal 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
47
forge.config.js
Normal 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
16
index.html
Normal 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
75
package.json
Normal 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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
36
public/callback.html
Normal file
36
public/callback.html
Normal 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"> →</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
BIN
public/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
66
shared/schema.d.ts
vendored
Normal file
66
shared/schema.d.ts
vendored
Normal 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
261
src/App.vue
Normal 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
1
src/assets/electron.svg
Normal 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
4
src/assets/ffmpeg.svg
Normal 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
1
src/assets/vue.svg
Normal 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 |
34
src/components/AppTitleBar.vue
Normal file
34
src/components/AppTitleBar.vue
Normal 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>
|
||||
37
src/components/ProgressBar.vue
Normal file
37
src/components/ProgressBar.vue
Normal 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>
|
||||
16
src/components/VButton.vue
Normal file
16
src/components/VButton.vue
Normal 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
10
src/main.ts
Normal 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
13
src/samples/node-api.ts
Normal 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
12
src/style.css
Normal 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
7
src/vite-env.d.ts
vendored
Normal 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
49
tailwind.config.js
Normal 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
20
tsconfig.json
Normal 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
10
tsconfig.node.json
Normal 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
75
vite.config.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user