diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 741f802..6735bc3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,19 +32,19 @@ jobs: include: - os: ubuntu-latest artifact_name: sixgrid-linux-amd64.tar.gz - asset_name: "sixgrid-${{ github.sha }}-linux-amd64.tar.gz" + asset_name: "sixgrid-linux-amd64.tar.gz" - os: ubuntu-latest artifact_name: sixgrid-linux-amd64.zip - asset_name: "sixgrid-${{ github.sha }}-linux-amd64.zip" + asset_name: "sixgrid-linux-amd64.zip" - os: ubuntu-latest artifact_name: "sixgrid-linux-amd64.AppImage" - asset_name: "sixgrid-${{ github.sha }}-linux-amd64.AppImage" + asset_name: "sixgrid-linux-amd64.AppImage" - os: windows-latest artifact_name: sixgrid-windows-amd64-setup.msi - asset_name: "sixgrid-${{ github.sha }}-win-amd64-setup.msi" + asset_name: "sixgrid-win-amd64-setup.msi" - os: windows-latest artifact_name: sixgrid-windows-amd64.zip - asset_name: "sixgrid-${{ github.sha }}-win-amd64.zip" + asset_name: "sixgrid-win-amd64.zip" steps: - name: Check out Git Repo diff --git a/src/main/config/configManager.ts b/src/main/config/configManager.ts new file mode 100644 index 0000000..8e0d328 --- /dev/null +++ b/src/main/config/configManager.ts @@ -0,0 +1,186 @@ +import { BrowserWindow, ipcMain } from 'electron' +import * as helpers from '../helpers' +import * as sharedHelper from '../../shared/sharedHelper' +import { ConfigFileMap, ConfigKeys, ConfigTypeMap, PostDataSetConfig } from '../../shared/config' +import { DefaultData as DefaultConfigData } from '../../shared/configDefault' +import path from 'path' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' + +export class ConfigManager { + static get instance() { + if (global.configManager == undefined) + { + global.configManager = new ConfigManager() + } + return global.configManager + } + + static get baseDirectory() { + return helpers.steamCloudConfigDirectory() + } + + /** + * @description + * Config Manager. + * + * Note: This class is a singleton and only one instance can be created. + * @throws + * Will throw the error `global.electronMainWindow is undefined!!!` when `global.electronMainWindow` hasn't been created yet. + * + * It will also throw an error when `global.configManager` is defined already. This is because ConfigManager is a singleton and only one instance can be created. + */ + constructor() { + if (global.electronMainWindow == undefined) + throw new Error('global.electronMainWindow is undefined!!!') + if (global.configManager != undefined) + throw new Error('Instance of global.configManager exists already!') + + global.configManager = this + + this.window = global.electronMainWindow + this.cache = sharedHelper.deepClone({}, DefaultConfigData) as ConfigTypeMap + + this.loadData() + + this.initIPC() + + this.saveLoop = setInterval(() => { + this.saveAll(true) + }, 5000) + } + + saveLoop: NodeJS.Timer + window: BrowserWindow + cache: ConfigTypeMap + /** + * @description + * Stores when a config was last modified via `ConfigManager.set()` + */ + lastModify: {[key in ConfigKeys]: number} = { + 'Authentication': 0, + 'User': 0, + 'Statistics': 0, + 'Keybind': 0 + } + /** + * @description + * Stores when a config was last saved via `ConfigManager.save()` + */ + lastSave: {[key in ConfigKeys]: number} = { + 'Authentication': 0, + 'User': 0, + 'Statistics': 0, + 'Keybind': 0 + } + pendingSave: {[key in ConfigKeys]: boolean} = { + 'Authentication': false, + 'User': false, + 'Statistics': false, + 'Keybind': false + } + + private loadData() + { + if (!existsSync(ConfigManager.baseDirectory)) + mkdirSync(ConfigManager.baseDirectory) + let entries = Object.entries(ConfigFileMap) as [ConfigKeys, string][] + for (let pair of entries) + { + let location = path.join(ConfigManager.baseDirectory, pair[1]) + if (!existsSync(location)) + { + writeFileSync(location, JSON.stringify(this.cache[pair[0]], null, ' ')) + } + + let data = readFileSync(location).toString() + let parsed = JSON.parse(data) + this.cache[pair[0]] = parsed + } + } + + private initIPC() + { + ipcMain.handle('config.get', (event, key: String) => + { + if (this.cache[key as ConfigKeys] == undefined) + throw new Error(`Key provided '${key}' does not exist`) + return this.cache[key as ConfigKeys] + }) + ipcMain.handle('config.getKeys', (event) => + { + return Object.entries(this.cache).map(v => v[0]) + }) + ipcMain.handle('config.set', (event, data: PostDataSetConfig) => + { + return this.set(data.key, data.data) + }) + + ipcMain.handle('config.saveAll', (event) => { + return this.saveAll() + }) + ipcMain.handle('config.save', (event, key: ConfigKeys) => { + return this.save(key) + }) + ipcMain.handle('config.getValue', (event, configKey: ConfigKeys, dataKey: any) => { + return this.getValue(configKey, dataKey) + }) + } + /** + * @description + * Save all cached configs. + * @param onlyPending Only save item if `pendingSave[key]` is true + */ + saveAll(onlyPending: boolean = false) { + for (let pair of Object.entries(this.cache)) { + if (onlyPending) { + if (!this.pendingSave[pair[0] as ConfigKeys]) + continue; + } + this.save(pair[0] as ConfigKeys) + } + } + + save(key: ConfigKeys) { + if (!existsSync(ConfigManager.baseDirectory)) + mkdirSync(ConfigManager.baseDirectory) + + let targetPath = path.join( + ConfigManager.baseDirectory, + ConfigFileMap[key]) + + let data = JSON.stringify(this.cache[key], null, ' ') + writeFileSync(targetPath, data) + + this.pendingSave[key] = false + this.lastSave[key] = Date.now() + } + + set(key: ConfigKeys, value: any): void + { + this.cache[key] = value + this.lastModify[key] = Date.now() + this.pendingSave[key] = true + } + get(key: ConfigKeys): void + { + if (this.cache[key] == undefined) + throw new Error(`Key '${key}' does not exist in cache`) + return sharedHelper.clone(this.cache[key]) + } + getValue(configKey: ConfigKeys, dataKey: any): any { + if (this.cache[configKey] == undefined) + throw new Error(`Key '${configKey}' does not exist in cache`) + let d = this.cache[configKey] as any + return d[dataKey] + } + + update(key: ConfigKeys): void + { + let data: any = this.get(key) + data = { + ...DefaultConfigData[key], + ...data + } + this.set(key, data) + } +} \ No newline at end of file diff --git a/src/main/flags.ts b/src/main/flags.ts new file mode 100644 index 0000000..a623cc9 --- /dev/null +++ b/src/main/flags.ts @@ -0,0 +1,34 @@ +import { app } from 'electron' +import * as os from 'os' + +export default { + customUrlEnable: app.commandLine.hasSwitch('url'), + customUrl: app.commandLine.getSwitchValue('url'), + + winUrl_dev: 'http://localhost:9080', + get winUrl() { + if (this.debugMode && this.customUrlEnable) + { + return this.customUrlEnable ? this.customUrl : this.winUrl_dev + } + else + { + return process.env.NODE_ENV === 'development' ? this.winUrl_dev : `file://${__dirname}/index.html` + } + }, + + get debugMode() { + return app.commandLine.hasSwitch('developer') + || app.commandLine.hasSwitch('dev') + || process.env.NODE_ENV == 'development' + }, + + + get steamworks() { + return app.commandLine.hasSwitch('steam') + }, + + get isSteamDeck() { + return os.release().toString().includes('valve') || app.commandLine.hasSwitch('deck') + } +} \ No newline at end of file diff --git a/src/main/global.d.ts b/src/main/global.d.ts index a0f37af..5a8141c 100644 --- a/src/main/global.d.ts +++ b/src/main/global.d.ts @@ -1,6 +1,8 @@ import type { IProductInformation } from '../shared' import type { BrowserWindow } from 'electron' -import type { GlobalShortcutData } from './globalShortcuts' +import type { GlobalShortcutData } from '../shared/config' +import type { ConfigManager } from './config/configManager' + declare global { var electronMainWindow: BrowserWindow|undefined @@ -9,5 +11,6 @@ declare global var __static: string var debugMode: boolean var globalShortcut_data: GlobalShortcutData + var configManager: ConfigManager } diff --git a/src/main/globalShortcuts.ts b/src/main/globalShortcuts.ts index 33153cf..9ca3349 100644 --- a/src/main/globalShortcuts.ts +++ b/src/main/globalShortcuts.ts @@ -3,12 +3,8 @@ import * as fs from 'fs' import * as helpers from './helpers' import { globalShortcut, app, ipcMain } from 'electron' -export interface GlobalShortcutData -{ - relaunch: Electron.Accelerator|null - debugOutline: Electron.Accelerator|null - safeReload: Electron.Accelerator|null -} +import { GlobalShortcutData } from '../shared/config' + interface GlobalShortcutActions { relaunch(): void diff --git a/src/main/helpers.ts b/src/main/helpers.ts index 85aee05..fb62577 100644 --- a/src/main/helpers.ts +++ b/src/main/helpers.ts @@ -1,11 +1,8 @@ import { app, dialog } from 'electron' import * as path from 'path' +import flags from './flags' const _ProductInformation = __PRODUCT_EXTENDED_INFORMATION -export function isDevelopmentMode () { - if (app.commandLine.hasSwitch('dev')) - return true - return process.env.NODE_ENV === 'development' -} + export function fetchTitle () { let value = `SixGrid v${__SIXGRID_PRODUCT_BUILD_VERSION} (${_ProductInformation.commitHashShort})` if (electronMainWindow != undefined) @@ -14,14 +11,8 @@ export function fetchTitle () { } export function safeReload () { if (electronMainWindow != undefined) - electronMainWindow.loadURL(winURL) + electronMainWindow.loadURL(flags.winUrl) } -export const winURL = (() => { - var value = isDevelopmentMode() - ? `http://localhost:9080` - : `file://${process.platform == 'win32' ? '/' : ''}${__dirname.replaceAll('\\', '/')}/index.html` - return value -})() export function relaunch () { app.relaunch() app.quit() @@ -36,43 +27,11 @@ export function relaunchConfirm () { `Relaunch` ] }) + if (btn == 1) { relaunch() } } -export function stringArrayCharacterLength (input: string[]) { - let length = 0 - for (let thing of input) { - length += thing.length - } - return length -} -export function paragraphSplit (input: string, maximumLineWidth: number) { - let resultList = [] /* string[][] */ - let inputSplitted = input.split(' ') - let buffer = [] /* input[] */ - for (let i = 0; i < inputSplitted.length; i++) { - let bufferCharLen = stringArrayCharacterLength(buffer) + buffer.length - if (bufferCharLen + inputSplitted[i].length + 1 > maximumLineWidth) { - resultList.push(buffer) - buffer = [] - } - buffer.push(inputSplitted[i]) - } - - resultList.push(buffer) - - let resultArray = [] - for (let i = 0; i < resultList.length; i++) { - let tmp = [] /* string[] */ - for (let x = 0; x < resultList[i].length; x++) { - tmp.push(resultList[i][x]) - } - let tmpString = tmp.join(' ') - resultArray.push(tmpString) - } - return resultArray.join('\n') -} export function steamCloudConfigDirectory() { let target = path.join(path.dirname(process.execPath), 'AppConfig') @@ -80,4 +39,4 @@ export function steamCloudConfigDirectory() { target = path.join(process.cwd(), 'AppConfig') } return target -} \ No newline at end of file +} diff --git a/src/main/index.d.ts b/src/main/index.d.ts index 603b01e..e6a0ee1 100644 --- a/src/main/index.d.ts +++ b/src/main/index.d.ts @@ -1,5 +1,8 @@ import type { IProductInformation } from '../shared' import type { BrowserWindow } from 'electron' +import type { GlobalShortcutData } from '../shared/config' +import type { ConfigManager } from './config/configManager' + declare global { namespace NodeJS { interface Global { @@ -8,6 +11,8 @@ declare global { __PRODUCT_EXTENDED_INFORMATION: IProductInformation __static: string debugMode: boolean + globalShortcut_data: GlobalShortcutData + configManager: ConfigManager } } } diff --git a/src/main/index.ts b/src/main/index.ts index da55320..4a1de58 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,9 +4,11 @@ import menu from './menu' import * as helpers from './helpers' import * as os from 'os' import * as globalShortcuts from './globalShortcuts' -let isSteamDeck: boolean = os.release().toString().includes('valve') +import { ConfigManager } from './config/configManager' -if (isSteamDeck) { +import flags from './flags' + +if (flags.isSteamDeck) { app.disableHardwareAcceleration() console.log(`Running on Steam Deck. Hardware Acceleration has been disabled due to some linux issues.`) } @@ -20,28 +22,21 @@ if (process.env.NODE_ENV !== 'development') { } ipcMain.handle('updateTitle', (event, data) => { + setTitle(data) +}) + +export function setTitle(append: string='') { if (global.electronMainWindow == undefined) return - if (data.length < 1) + + if (append.length < 1) global.electronMainWindow.setTitle(helpers.fetchTitle()) else - global.electronMainWindow.setTitle(helpers.fetchTitle() + ` - ${data}`) -}) + global.electronMainWindow.setTitle(helpers.fetchTitle() + ` - ${append}`) +} -global.debugMode = app.commandLine.hasSwitch('developer') +global.debugMode = flags.debugMode app.commandLine.appendSwitch('in-process-gpu') -let customURL_enable = app.commandLine.hasSwitch('url') -let customURL = app.commandLine.getSwitchValue('url') - -const winURL_dev = 'http://localhost:9080' -let winURL: string = '' -if (global.debugMode && customURL_enable) -{ - winURL = customURL_enable ? customURL : winURL_dev -} -else -{ - winURL = process.env.NODE_ENV === 'development' ? winURL_dev : `file://${__dirname}/index.html` -} + function createWindow () { app.allowRendererProcessReuse = false global.electronMainWindow = new BrowserWindow({ @@ -60,19 +55,26 @@ function createWindow () { contextIsolation: false } }) + + global.configManager = new ConfigManager() + globalShortcuts.init() - if (isSteamDeck) { + if (flags.isSteamDeck) { global.electronMainWindow.webContents.setFrameRate(60) console.log(`Set framerate to 60fps`) } + + // set title global.electronMainWindow.setMenu(null) Menu.setApplicationMenu(Menu.buildFromTemplate(menu)) global.electronMainWindow.setTitle(helpers.fetchTitle()) - global.electronMainWindow.loadURL(winURL) + global.electronMainWindow.loadURL(flags.winUrl) + // save config on close global.electronMainWindow.on('closed', () => { delete global.electronMainWindow + ConfigManager.instance.saveAll(false) }) // Send uncaught exceptions to renderer @@ -81,6 +83,7 @@ function createWindow () { global.electronMainWindow.webContents.send('uncaughtException', JSON.stringify(error)) }) ipcMain.on('restart', () => { + ConfigManager.instance.saveAll(false) helpers.relaunch() }) } diff --git a/src/main/menu.ts b/src/main/menu.ts index 786f646..f9678cc 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -9,6 +9,7 @@ const menuTemplate: any = [ { type: 'separator' }, { label: 'Relaunch', + accelerator: global.globalShortcut_data.relaunch, click: () => { helpers.relaunch() } diff --git a/src/renderer/ConfigInit.ts b/src/renderer/ConfigInit.ts index e1ad94b..a223453 100644 --- a/src/renderer/ConfigInit.ts +++ b/src/renderer/ConfigInit.ts @@ -1,6 +1,7 @@ import * as path from 'path' import * as fs from 'fs' import Configuration from './Configuration' +import { DefaultData } from '../shared/configDefault' export interface IConfigTemplate { filename: string @@ -11,95 +12,22 @@ export const configStoreProfiles: IConfigTemplate[] = [ { filename: 'authProfile.json', key: 'Authentication', - data: { - items: [ - { - auth: { - login: '', - apikey: '', - enabled: false - }, - endpoint: 'https://e926.net' - }, - { - auth: { - login: '', - apikey: '', - enabled: false - }, - endpoint: 'https://e621.net' - } - ], - _current: 0 - } + data: DefaultData.Authentication }, { filename: 'config.json', key: 'User', - data: { - media: { - autoplay: true, - loop: true - }, - get downloadFolder() { - return path.join(require('electron').remote.app.getPath('home'), 'Downloads', 'sixgrid') - }, - saveMetadata: false, - tagBlacklist: [], - ratingFilter: 'none', - preloadPageCount: 1, - preloadStartIndex: 0, - highQualityPreview: false, - sortByScore: false, - sortByFavorite: false, - ratingSafe: false, - ratingQuestionable: false, - ratingExplicit: false, - zoomFactor: 1.0, - mainProcShortcuts: { - relaunch: 'F10', - debugOutline: 'F9', - safeReload: 'F8' - } - } + data: DefaultData.User }, { filename: 'stats.json', key: 'Statistics', - data: { - metricStore: {} - } + data: DefaultData.Statistics }, { filename: 'keybind.json', key: 'Keybind', - data: { - currentProfile: 'default', - currentProfileData: { - Id: 'gvvqMEmj', - Name: 'Default', - Binds: [ - { - Id: 'METs7gPv', - Chords: [[39]], - Enable: true, - Channel: 'item:next' - }, - { - Id: 'g0FPjBOz', - Chords: [[37]], - Enable: true, - Channel: 'item:previous' - }, - { - Id: '6mRXSbBE', - Chords: [[27]], - Enable: true, - Channel: 'view:close' - } - ] - } - } + data: DefaultData.Keybind } ] @@ -109,6 +37,10 @@ export function Initialize() { let location = path.join(AppData.SteamCloudLocations.Config, item.filename) + if (!fs.existsSync(AppData.SteamCloudLocations.Config)) { + fs.mkdirSync(AppData.SteamCloudLocations.Config, { recursive: true }) + } + if (!fs.existsSync(location)) fs.writeFileSync(location, JSON.stringify(item.data, null, ' ')) @@ -117,6 +49,13 @@ export function Initialize() global.AppData.CloudConfig[item.key].default(item.data) global.AppData.CloudConfig[item.key].write() } + + if (global.AppData.CloudConfig.User.get('downloadFolder').length < 1) + { + global.AppData.CloudConfig.User.set('downloadFolder', path.join(require('electron').remote.app.getPath('home'), 'Downloads', 'sixgrid')) + global.AppData.CloudConfig.User.write() + } + } export function ResetItem(name: string) { diff --git a/src/renderer/ConfigTemplate.d.ts b/src/renderer/ConfigTemplate.d.ts deleted file mode 100644 index 1019b08..0000000 --- a/src/renderer/ConfigTemplate.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IClientAuthOptions } from 'libsixgrid/dist/src/Client' -import { PostRating } from 'libsixgrid' -import { MetricManagerData } from './MetricManager' -import { KeybindProfile } from './Keybinder/KeybindProfile' - -export interface AuthTemplate -{ - auth: IClientAuthOptions - endpoint: string -} -export interface IConfig_AuthProfile -{ - items: AuthTemplate[] - _current: number -} - -export interface IConfig_User -{ - media: { - autoplay: boolean, - loop: boolean - }, - downloadFolder: string - saveMetadata: boolean - tagBlacklist: string[] - ratingFilter: PostRating - preloadPageCount: number - preloadStartIndex: number - highQualityPreview: boolean - sortByScore: boolean - sortByFavorite: boolean - ratingSafe: boolean - ratingQuestionable: boolean - ratingExplicit: boolean - zoomFactor: number // float -} - -export interface IConfig_Stats -{ - metricStore: MetricManagerData -} -export interface IConfig_Keybind -{ - currentProfile: string - currentProfileData: KeybindProfile -} \ No newline at end of file diff --git a/src/renderer/Keybinder/KeybindItem.ts b/src/renderer/Keybinder/KeybindItem.ts index 38df8e6..7e95d3a 100644 --- a/src/renderer/Keybinder/KeybindItem.ts +++ b/src/renderer/Keybinder/KeybindItem.ts @@ -1,7 +1,7 @@ -import type {KeybindChord, KeybindChannel} from './index' +import { IKeybindItem, KeybindChord } from '../../shared/keybind' const toolbox = require('tinytoolbox') -export class KeybindItem +export class KeybindItem implements IKeybindItem { public Id: string = toolbox.stringGen(8) public Chords: KeybindChord[] = [] diff --git a/src/renderer/Keybinder/KeybindManager.ts b/src/renderer/Keybinder/KeybindManager.ts index 15b79a3..130bc27 100644 --- a/src/renderer/Keybinder/KeybindManager.ts +++ b/src/renderer/Keybinder/KeybindManager.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events' -import { KeybindChord, Keystate } from './index' +import { KeybindChord, Keystate } from '../../shared/keybind' import { KeybindItem } from './KeybindItem' import { KeybindProfile } from './KeybindProfile' const enumKeycode: {[key: string]: number} = require('./enum.Keycode.js') diff --git a/src/renderer/Keybinder/KeybindProfile.ts b/src/renderer/Keybinder/KeybindProfile.ts index 9f5bf2e..271bc66 100644 --- a/src/renderer/Keybinder/KeybindProfile.ts +++ b/src/renderer/Keybinder/KeybindProfile.ts @@ -1,11 +1,8 @@ -import {KeybindItem} from './KeybindItem' +import { IKeybindProfile } from '../../shared/keybind' +import { KeybindItem } from './KeybindItem' + const toolbox = require('tinytoolbox') -export interface IKeybindProfile -{ - Id: string - Name: string -} export class KeybindProfile implements IKeybindProfile { Id: string = toolbox.stringGen(8) diff --git a/src/renderer/Keybinder/index.ts b/src/renderer/Keybinder/index.ts deleted file mode 100644 index 54b0244..0000000 --- a/src/renderer/Keybinder/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export declare type KeybindChord = number[] -export declare type KeybindChannel = - 'unknown' | - 'item:next' | - 'item:previous' | - 'view:close' - - - -export enum Keystate -{ - Up, - Down -} \ No newline at end of file diff --git a/src/renderer/MetricManager.ts b/src/renderer/MetricManager.ts index 1f5a438..353ed58 100644 --- a/src/renderer/MetricManager.ts +++ b/src/renderer/MetricManager.ts @@ -1,8 +1,7 @@ import { EventEmitter } from "events" -import { ISteamMetric } from "./SteamworksIntergration" +import { MetricManagerData } from "../shared/metricManager" import * as ElectronLog from 'electron-log' let log: ElectronLog.LogFunctions; -export type MetricManagerData = {[key: string]: ISteamMetric} export default class MetricManager extends EventEmitter { public constructor() diff --git a/src/renderer/SteamworksIntergration.ts b/src/renderer/SteamworksIntergration.ts index 33260c1..0aa5d61 100644 --- a/src/renderer/SteamworksIntergration.ts +++ b/src/renderer/SteamworksIntergration.ts @@ -1,23 +1,9 @@ import * as greenworks from 'greenworks' import {EventEmitter} from 'events' -import { MetricManagerData } from './MetricManager' +import { ISteamMetric } from '../shared/steamworks' import * as ElectronLog from 'electron-log' let log: ElectronLog.LogFunctions; -export type SteamMetricType = 'int'|'float' -export interface ISteamMetric -{ - min: number, - max: number, - default: number, - value: number, - incrementOnly: boolean, - name: string, - increment: boolean, - type: SteamMetricType - process?: SteamMetricProcess -} -export type SteamMetricProcess = (steamworks: typeof greenworks, scope: any, key: string) => any export default class Steamworks extends EventEmitter { public static AppID = 1992810 @@ -36,12 +22,15 @@ export default class Steamworks extends EventEmitter { return new Error(`[Steamworks->${thing.id}] ${thing.message}`) } Initialize() { + // ignore init when steamworks is disabled if (!AppData.AllowSteamworks) { log.debug('Disabled') return } if (this.hasInitalized) return + + // attempt to initialize greenworks and safely exit if failed try { let response = this.Greenworks.init() if (response) { diff --git a/src/renderer/configBridgeClient.ts b/src/renderer/configBridgeClient.ts new file mode 100644 index 0000000..4106a54 --- /dev/null +++ b/src/renderer/configBridgeClient.ts @@ -0,0 +1,239 @@ +import { ipcRenderer } from 'electron' +import { ConfigKeys, IConfig_AuthProfile } from '../shared/config' + +interface IGetResponse +{ + result: GetResponseData, + success: boolean, + errorMessage: String|null, + givenReason: any +} +export type GetResponseData = T|undefined|null +class GetResponse implements IGetResponse +{ + constructor( + result: GetResponseData = null, + success: boolean = false, + errorMessage: String|null = null, + givenReason: any = null) + { + this.result = result + this.success = success + this.errorMessage = errorMessage + this.givenReason = givenReason + } + + result: GetResponseData = null + success: boolean = false + errorMessage: String|null = null + givenReason: any = null +} + +interface ISetResponse +{ + result: any, + success: boolean, + errorMessage: String|null, + givenReason: any +} +class SetResponse implements ISetResponse +{ + constructor( + result: any = null, + success: boolean = false, + errorMessage: String|null = null, + givenReason: any = null) + { + this.result = result + this.success = success + this.errorMessage = errorMessage + this.givenReason = givenReason + } + result: any = null + success: boolean = false + errorMessage: String|null = null + givenReason: any = null +} + +interface IResponse +{ + result: T|null, + success: boolean, + errorMessage: String|null, + givenReason: any +} +function CreateResponse( + result: T|null = null, + success: boolean = false, + errorMessage: String|null = null, + givenReason: any = null): IResponse +{ + let data = { + result, + success, + errorMessage, + givenReason + } + return data +} + +export class configBridgeClient +{ + getConfig(key: 'Authentication', timeout: number): Promise> + getConfig(key: 'User', timeout: number): Promise> + getConfig(key: 'Statistics', timeout: number): Promise> + getConfig(key: 'Keybind', timeout: number): Promise> + /** + * @description + * Fetch config data from Main Thread + * @param key Key of the config to fetch + * @param timeout Timeout, measured in milliseconds (default to 5s) + * @returns @see IGetResponse + */ + getConfig(key: ConfigKeys, timeout: number = 5000): Promise> + { + return new Promise((resolve, reject) => + { + var hasResolved = false + ipcRenderer.invoke('config.get', key) + .then((data) => + { + hasResolved = true + + let response = new GetResponse(data, true) + resolve(response) + }) + .catch((reason) => + { + hasResolved = true + + let response = new GetResponse(null, false, "Caught on ipcRenderer.invoke('config.get')", reason) + reject(response) + }) + + setTimeout(() => { + if (!hasResolved) + { + let response = new GetResponse(null, false, `Timeout (${timeout}ms) on ipcRenderer.invoke('config.get', '${key}')`) + reject(response) + } + }, timeout) + }) + } + + getValue(configKey: ConfigKeys, dataKey: any, timeout: number = 5000): Promise> + { + return new Promise((resolve, reject) => + { + var hasResolved = false + ipcRenderer.invoke('config.getValue', configKey, dataKey) + .then((data) => + { + hasResolved = true + + let response = new GetResponse(data, true) + resolve(response) + }) + .catch((reason) => + { + hasResolved = true + + let response = new GetResponse(null, false, "Caught on ipcRenderer.invoke('config.getValue')", reason) + reject(response) + }) + + setTimeout(() => { + if (!hasResolved) + { + let response = new GetResponse(null, false, `Timeout (${timeout}ms) on ipcRenderer.invoke('config.Value', '${configKey}', '${dataKey}')`) + reject(response) + } + }, timeout) + }) + } + + /** + * @description + * Get available config keys + * @param timeout Timeout, measured in milliseconds (default to 5s) + * @returns List of config keys when `IResponse.success` is true + */ + getKeys(timeout: number = 5000): Promise> + { + return new Promise((resolve, reject) => + { + var hasResolved = false + ipcRenderer.invoke('config.getKeys') + .then((data) => + { + hasResolved = true + + let response = CreateResponse(data, true) + resolve(response) + }) + .catch((reason) => + { + hasResolved = true + + let response = CreateResponse(null, false, "Caught on ipcRenderer.invoke('config.getKeys')", reason) + reject(response) + }) + + setTimeout(() => { + if (!hasResolved) + { + let response = new GetResponse(null, false, `Timeout (${timeout}ms) on ipcRenderer.invoke('config.getKeys')`) + reject(response) + } + }, timeout) + }) + } + + + set(key: 'Authentication', data: IConfig_AuthProfile, timeout: number): Promise + set(key: 'User', data: IConfig_User, timeout: number): Promise + set(key: 'Statistics', data: IConfig_Stats, timeout: number): Promise + set(key: 'Keybind', data: IConfig_Keybind, timeout: number): Promise + /** + * @description + * Set the data of a specific config. + * @param key Config key to set + * @param data Data of the config to set + * @param timeout Timeout, measured in milliseconds (default to 5s) + * @returns @see ISetResponse + */ + set(key: ConfigKeys, data: T, timeout: number = 5000): Promise + { + return new Promise((resolve, reject) => + { + var hasResolved = false + let postData = { + key, + data + } + ipcRenderer.invoke('config.set', postData) + .then((data) => + { + hasResolved = true + + let response = new SetResponse(data, true) + resolve(response) + }) + .catch((reason) => + { + hasResolved = true + + let response = new SetResponse(null, false, "Caught on ipcRenderer.invoke()", reason) + reject(response) + }) + setTimeout(() => { + if (!hasResolved) + { + let response = new SetResponse(null, false, `Timeout (${timeout}ms) on ipcRenderer.invoke('config.set') with key of '${key}'`) + reject(response) + } + }, timeout) + + }) + } +} \ No newline at end of file diff --git a/src/renderer/index.d.ts b/src/renderer/index.d.ts index 9f1b75a..73c6603 100644 --- a/src/renderer/index.d.ts +++ b/src/renderer/index.d.ts @@ -2,7 +2,7 @@ import Configuration, { IConfiguration } from './Configuration' import type {EventEmitter} from 'events' import Post from 'libsixgrid/dist/src/Post' import {IProductInformation} from '../shared' -import { IConfig_AuthProfile, IConfig_Stats, IConfig_User, IConfig_Keybind } from './ConfigTemplate' +import { IConfig_AuthProfile, IConfig_Stats, IConfig_User, IConfig_Keybind } from '../shared/config' import MetricManager from './MetricManager' import Steamworks from './SteamworksIntergration' import { KeybindManager } from './Keybinder/KeybindManager' diff --git a/src/shared/config.ts b/src/shared/config.ts new file mode 100644 index 0000000..6350937 --- /dev/null +++ b/src/shared/config.ts @@ -0,0 +1,114 @@ +import { IClientAuthOptions } from 'libsixgrid/dist/src/Client' +import { PostRating } from 'libsixgrid' +import { MetricManagerData } from './metricManager' +import { KeybindProfile } from '../renderer/Keybinder/KeybindProfile' + +export interface PostDataSetConfig +{ + key: ConfigKeys, + data: any +} + +export interface ConfigTypeMap { + 'Authentication': IConfig_AuthProfile + 'User': IConfig_User + 'Statistics': IConfig_Stats + 'Keybind': IConfig_Keybind +} +export declare type ConfigKeys = + 'Authentication' | + 'User' | + 'Statistics' | + 'Keybind' + +export const ConfigFileMap: {[key in ConfigKeys]: string} = { + 'Authentication': 'authProfile.json', + 'User': 'config.json', + 'Statistics': 'stats.json', + 'Keybind': 'keybind.json' +} + +export interface AuthTemplate +{ + auth: IClientAuthOptions + endpoint: string +} +export interface IConfig_AuthProfile +{ + items: AuthTemplate[] + /** + * @description + * Index of `items` for the currently used authentication profile + */ + _current: number +} + +export interface GlobalShortcutData +{ + relaunch: Electron.Accelerator|null + debugOutline: Electron.Accelerator|null + safeReload: Electron.Accelerator|null +} +export interface IConfig_User +{ + media: { + autoplay: boolean, + loop: boolean + }, + /** + * @description + * Folder to download to + */ + downloadFolder: string + saveMetadata: boolean + /** + * @description + * Additional tags to filter posts with + */ + tagBlacklist: string[] + ratingFilter: PostRating + preloadPageCount: number + preloadStartIndex: number + /** + * @description + * Use full file instead of preview file when loading + */ + highQualityPreview: boolean + sortByScore: boolean + sortByFavorite: boolean + + /** + * @description + * Allow posts tagged with `rating:s` + */ + ratingSafe: boolean + /** + * @description + * Allow posts tagged with `rating:q` + */ + ratingQuestionable: boolean + /** + * @description + * Allow posts tagged with `rating:e` + */ + ratingExplicit: boolean + + /** + * @description + * Float between `0.5` and `3.0` + */ + zoomFactor: number + + mainProcShortcuts: GlobalShortcutData +} + +export interface IConfig_Stats +{ + metricStore: MetricManagerData +} +export interface IConfig_Keybind +{ + currentProfile: string + currentProfileData: KeybindProfile + profiles: {[key: string]: KeybindProfile} +} \ No newline at end of file diff --git a/src/shared/configDefault.ts b/src/shared/configDefault.ts new file mode 100644 index 0000000..9405761 --- /dev/null +++ b/src/shared/configDefault.ts @@ -0,0 +1,80 @@ +import { ConfigTypeMap } from "./config" + +export const DefaultData: ConfigTypeMap = { + 'Authentication': { + items: [ + { + auth: { + login: '', + apikey: '', + enabled: false + }, + endpoint: 'https://e926.net' + }, + { + auth: { + login: '', + apikey: '', + enabled: false + }, + endpoint: 'https://e621.net' + } + ], + _current: 0 + }, + 'User': { + media: { + autoplay: true, + loop: true + }, + downloadFolder: '', + saveMetadata: false, + tagBlacklist: [], + ratingFilter: 'none', + preloadPageCount: 1, + preloadStartIndex: 0, + highQualityPreview: false, + sortByScore: false, + sortByFavorite: false, + ratingSafe: false, + ratingQuestionable: false, + ratingExplicit: false, + zoomFactor: 1.0, + mainProcShortcuts: { + relaunch: 'F10', + debugOutline: 'F9', + safeReload: 'F8' + } + }, + 'Statistics': { + metricStore: {} + }, + 'Keybind': { + currentProfile: 'default', + currentProfileData: { + Id: 'gvvqMEmj', + Name: 'Default', + Binds: [ + { + Id: 'METs7gPv', + Chords: [[39]], + Enable: true, + Channel: 'item:next' + }, + { + Id: 'g0FPjBOz', + Chords: [[37]], + Enable: true, + Channel: 'item:previous' + }, + { + Id: '6mRXSbBE', + Chords: [[27]], + Enable: true, + Channel: 'view:close' + } + ] + }, + profiles: {} + } +} \ No newline at end of file diff --git a/src/shared/keybind.ts b/src/shared/keybind.ts new file mode 100644 index 0000000..eb9a392 --- /dev/null +++ b/src/shared/keybind.ts @@ -0,0 +1,23 @@ +export interface IKeybindProfile +{ + Id: string + Name: string + Binds: IKeybindItem[] +} +export interface IKeybindItem { + Id: string + Chords: KeybindChord[] + Enable: boolean + Channel: string +} +export declare type KeybindChord = number[] +export declare type KeybindChannel = + 'unknown' | + 'item:next' | + 'item:previous' | + 'view:close' +export enum Keystate +{ + Up, + Down +} \ No newline at end of file diff --git a/src/shared/metricManager.ts b/src/shared/metricManager.ts new file mode 100644 index 0000000..330dd90 --- /dev/null +++ b/src/shared/metricManager.ts @@ -0,0 +1,2 @@ +import { ISteamMetric } from "./steamworks" +export type MetricManagerData = {[key: string]: ISteamMetric} \ No newline at end of file diff --git a/src/shared/sharedHelper.ts b/src/shared/sharedHelper.ts new file mode 100644 index 0000000..9b0cdee --- /dev/null +++ b/src/shared/sharedHelper.ts @@ -0,0 +1,64 @@ +import * as path from 'path' + +export function stringArrayCharacterLength (input: string[]) { + let length = 0 + for (let thing of input) { + length += thing.length + } + return length +} +export function paragraphSplit (input: string, maximumLineWidth: number) { + let resultList = [] /* string[][] */ + let inputSplitted = input.split(' ') + let buffer = [] /* input[] */ + for (let i = 0; i < inputSplitted.length; i++) { + let bufferCharLen = stringArrayCharacterLength(buffer) + buffer.length + if (bufferCharLen + inputSplitted[i].length + 1 > maximumLineWidth) { + resultList.push(buffer) + buffer = [] + } + buffer.push(inputSplitted[i]) + } + + resultList.push(buffer) + + let resultArray = [] + for (let i = 0; i < resultList.length; i++) { + let tmp = [] /* string[] */ + for (let x = 0; x < resultList[i].length; x++) { + tmp.push(resultList[i][x]) + } + let tmpString = tmp.join(' ') + resultArray.push(tmpString) + } + return resultArray.join('\n') +} +export function steamCloudConfigDirectory() { + let target = path.join(path.dirname(process.execPath), 'AppConfig') + if (path.basename(process.execPath).startsWith('electron')) { + target = path.join(process.cwd(), 'AppConfig') + } + return target +} +function isObject(item: any): boolean { + return (item && typeof item === 'object' && !Array.isArray(item)); +} +export function deepClone(target: any, source: any): any { + let output = Object.assign({}, target); + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach(key => { + if (isObject(source[key])) { + if (!(key in target)) + Object.assign(output, { [key]: source[key] }); + else + output[key] = deepClone(target[key], source[key]); + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + return output; +} +export function clone(data: any): any { + return JSON.parse(JSON.stringify(data)) +} \ No newline at end of file diff --git a/src/shared/steamworks.d.ts b/src/shared/steamworks.d.ts new file mode 100644 index 0000000..17ccce3 --- /dev/null +++ b/src/shared/steamworks.d.ts @@ -0,0 +1,15 @@ +import type * as greenworks from 'greenworks' +export interface ISteamMetric +{ + min: number, + max: number, + default: number, + value: number, + incrementOnly: boolean, + name: string, + increment: boolean, + type: SteamMetricType + process?: SteamMetricProcess +} +export type SteamMetricProcess = (steamworks: typeof greenworks, scope: any, key: string) => any +export type SteamMetricType = 'int'|'float' \ No newline at end of file