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
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user