From a55af1537af0c7d026713674126be00773d33ddb Mon Sep 17 00:00:00 2001 From: Nekiro Date: Thu, 20 Jan 2022 02:03:21 +0100 Subject: [PATCH] Move project to typescript --- .gitignore | 5 +- App.ts | 4 ++ README.md | 4 +- js/mainRenderer.js | 75 ---------------------- main.js | 91 -------------------------- package.json | 58 +++++++++-------- src/Main.ts | 107 +++++++++++++++++++++++++++++++ {assets => src/assets}/icon.ico | Bin {css => src/css}/style.css | 0 {html => src/html}/index.html | 5 +- js/ipcMain.js => src/ipc/main.ts | 6 +- src/ipc/renderer.ts | 91 ++++++++++++++++++++++++++ {rcc => src/rcc}/rcc.exe | Bin js/reader.js => src/reader.ts | 83 +++++++++++++----------- src/types.ts | 11 ++++ tsconfig.json | 13 ++++ 16 files changed, 316 insertions(+), 237 deletions(-) create mode 100644 App.ts delete mode 100644 js/mainRenderer.js delete mode 100644 main.js create mode 100644 src/Main.ts rename {assets => src/assets}/icon.ico (100%) rename {css => src/css}/style.css (100%) rename {html => src/html}/index.html (84%) rename js/ipcMain.js => src/ipc/main.ts (57%) create mode 100644 src/ipc/renderer.ts rename {rcc => src/rcc}/rcc.exe (100%) rename js/reader.js => src/reader.ts (64%) create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 75ada69..9943b87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules -dist package-lock.json -*.rcc \ No newline at end of file +*.rcc +out/ +build/ \ No newline at end of file diff --git a/App.ts b/App.ts new file mode 100644 index 0000000..605c49a --- /dev/null +++ b/App.ts @@ -0,0 +1,4 @@ +import { app, BrowserWindow } from 'electron'; +import Main from './src/Main'; + +Main.main(app, BrowserWindow); diff --git a/README.md b/README.md index 69d64ab..2c4af33 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,12 @@ Install the dependencies and start the main script. ```sh cd nekiro-rcc-editor npm i +npm run compile npm run start ``` ## Compilation + Follow above instructions, but instead of **start** write **build**, build files are written to /dist directory ```sh @@ -35,6 +37,6 @@ npm run build If you like my work and respect my time, consider becoming [Github Sponsor](https://github.com/sponsors/nekiro). - ### Credits + - [rccextended](https://github.com/zedxxx/rccextended) for awesome QT rcc lib diff --git a/js/mainRenderer.js b/js/mainRenderer.js deleted file mode 100644 index ee3fc95..0000000 --- a/js/mainRenderer.js +++ /dev/null @@ -1,75 +0,0 @@ -//renderer process -const { ipcRenderer } = require('electron'); - -const preview = document.querySelector('.preview'); -const list = document.querySelector('.list'); -let focused = null; - -preview.addEventListener('drop', (event) => { - event.preventDefault(); - event.stopPropagation(); - - if (!focused) { - return; - } - - ipcRenderer.send('replace-image', { - index: parseInt(focused.id.split('-')[1], 10), - path: event.dataTransfer.files[0].path, - }); -}); - -preview.addEventListener('dragover', (e) => { - e.preventDefault(); - e.stopPropagation(); -}); - -ipcRenderer.on('update-preview', (event, data) => { - preview.src = `data:image/png;base64,${Buffer.from(data).toString('base64')}`; -}); - -ipcRenderer.on('update-miniature', (event, { index, data }) => { - list.querySelector( - `#btn-${index} > img` - ).src = `data:image/png;base64,${Buffer.from(data).toString('base64')}`; -}); - -ipcRenderer.on('populate-list', (event, images) => { - focused = null; - - while (list.firstChild) { - list.removeChild(list.firstChild); - } - - for (const [index, image] of images.entries()) { - if (!image.isImage) { - continue; - } - - const btn = document.createElement('button'); - btn.innerText = image.name; - btn.id = `btn-${index}`; - btn.onclick = (event) => { - if (focused == event.target) { - return; - } - - if (focused) { - focused.classList.remove('focused'); - } - - focused = event.target; - focused.classList.add('focused'); - ipcRenderer.send('get-image-data', index); - }; - - const img = document.createElement('img'); - img.className = 'miniature'; - img.src = `data:image/png;base64,${Buffer.from(image.data).toString( - 'base64' - )}`; - btn.appendChild(img); - - list.appendChild(btn); - } -}); diff --git a/main.js b/main.js deleted file mode 100644 index 179b9fd..0000000 --- a/main.js +++ /dev/null @@ -1,91 +0,0 @@ -const { app, Menu, BrowserWindow, dialog, shell } = require('electron'); -const { loadRcc, saveRcc, extractToPng } = require('./js/reader'); -require('./js/ipcMain'); - -const createMainWindow = () => { - const mainWindow = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - }, - title: "Nekiro's Rcc Editor", - show: false, - }); - - const template = [ - { - label: 'Load rcc', - click: async () => { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openFile'], - filters: [{ name: 'Rcc File Type', extensions: ['rcc'] }], - }); - - if (!result.canceled) { - loadRcc(result.filePaths.pop()); - } - }, - }, - { - label: 'Extract assets', - click: async () => { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory', 'createDirectory'], - }); - - if (!result.canceled) { - extractToPng(result.filePaths.pop()); - } - }, - }, - { - label: 'Save rcc', - click: () => saveRcc(), - }, - { - label: 'Save rcc as', - click: async () => { - const result = await dialog.showSaveDialog(mainWindow, { - filters: [{ name: 'Rcc File Type', extensions: ['rcc'] }], - }); - - if (!result.canceled) { - saveRcc(result.filePath); - } - }, - }, - { - label: 'Donate', - click: () => shell.openExternal('https://github.com/sponsors/nekiro'), - }, - ]; - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - - mainWindow.loadURL(`file://${__dirname}/html/index.html`); - - mainWindow.webContents.once('did-finish-load', () => { - mainWindow.show(); - }); - - // Open the DevTools. - // mainWindow.webContents.openDevTools(); - return mainWindow; -}; - -app.whenReady().then(() => { - createMainWindow(); - - app.on('activate', () => { - // On macOS 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) createMainWindow(); - }); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') app.quit(); -}); diff --git a/package.json b/package.json index 4fb93d5..c1d4862 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,45 @@ { - "name": "nekiros_rcc_editor", + "name": "nekirosrcceditor", "version": "1.0.0", "description": "edit qt .rcc files", - "main": "main.js", + "main": "build/App.js", "scripts": { - "start": "electron .", - "pack": "electron-builder --dir", - "build": "electron-builder" + "start": "electron-forge start", + "compile": "tsc && copyfiles src/html/*.html src/css/*.css src/assets/* src/rcc/rcc.exe build", + "package": "electron-forge package", + "build": "npm run compile && electron-forge make" }, "author": "Nekiro", "license": "MIT", "devDependencies": { - "prettier": "^2.5.1" + "@electron-forge/cli": "^6.0.0-beta.63", + "@electron-forge/maker-squirrel": "^6.0.0-beta.63", + "@tsconfig/recommended": "^1.0.1", + "@types/electron": "^1.6.10", + "@types/fs-extra": "^9.0.13", + "electron": "^13.0.1", + "prettier": "^2.5.1", + "typescript": "^4.5.4" + }, + "dependencies": { + "electron-squirrel-startup": "^1.0.0", + "fs-extra": "^10.0.0" }, - "build": { - "appId": "com.electron.nekiros_rcc_editor", - "icon": "./assets/icon.ico", - "win": { - "target": { - "target": "nsis", - "arch": [ - "x64", - "ia32" - ] + "config": { + "forge": { + "packagerConfig": { + "icon": "./build/src/assets/icon.ico", + "extraResource": "./build/src/rcc", + "asar": true }, - "extraResources": [ - "rcc" + "makers": [ + { + "name": "@electron-forge/maker-squirrel", + "config": { + "name": "NekirosRccEditor" + } + } ] - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true } - }, - "dependencies": { - "electron": "^13.0.1", - "electron-builder": "^22.10.5", - "fs-extra": "^10.0.0" } } diff --git a/src/Main.ts b/src/Main.ts new file mode 100644 index 0000000..6fbee86 --- /dev/null +++ b/src/Main.ts @@ -0,0 +1,107 @@ +import { BrowserWindow, Menu, dialog, shell, app } from 'electron'; +import { loadRcc, saveRcc, extractToPng } from './reader'; +import path from 'path'; +require('./ipc/main'); + +export default class Main { + static mainWindow: Electron.BrowserWindow; + static application: Electron.App; + static BrowserWindow: any; + + private static onWindowAllClosed() { + if (process.platform !== 'darwin') { + Main.application.quit(); + } + } + + private static createWindow() { + Main.mainWindow = new Main.BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + title: "Nekiro's Rcc Editor", + show: false, + }); + + const template = [ + { + label: 'Load rcc', + click: async () => { + const result: Electron.OpenDialogReturnValue = + await dialog.showOpenDialog(Main.mainWindow, { + defaultPath: path.join( + app.getPath('appData'), + '../Local/Tibia/packages/Tibia/bin' + ), + properties: ['openFile'], + filters: [{ name: 'Rcc File Type', extensions: ['rcc'] }], + }); + + if (!result.canceled) { + loadRcc(result.filePaths.pop() ?? null); + } + }, + }, + { + label: 'Extract assets', + click: async () => { + const result: Electron.OpenDialogReturnValue = + await dialog.showOpenDialog(Main.mainWindow, { + properties: ['openDirectory', 'createDirectory'], + }); + + if (!result.canceled) { + extractToPng(result.filePaths.pop() ?? null); + } + }, + }, + { + label: 'Save rcc', + click: () => saveRcc(), + }, + { + label: 'Save rcc as', + click: async () => { + const result = await dialog.showSaveDialog(Main.mainWindow, { + filters: [{ name: 'Rcc File Type', extensions: ['rcc'] }], + }); + + if (!result.canceled) { + saveRcc(result.filePath); + } + }, + }, + { + label: 'Donate', + click: () => shell.openExternal('https://github.com/sponsors/nekiro'), + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + } + + private static onReady() { + // create window + Main.createWindow(); + + // load layout + Main.mainWindow.loadURL(`file://${__dirname}/html/index.html`); + + // show when its ready + Main.mainWindow.webContents.once('did-finish-load', () => { + Main.mainWindow.show(); + }); + + //Main.mainWindow.webContents.openDevTools(); + } + + static main(app: Electron.App, browserWindow: typeof BrowserWindow) { + Main.BrowserWindow = browserWindow; + Main.application = app; + Main.application.on('window-all-closed', Main.onWindowAllClosed); + Main.application.on('ready', Main.onReady); + } +} diff --git a/assets/icon.ico b/src/assets/icon.ico similarity index 100% rename from assets/icon.ico rename to src/assets/icon.ico diff --git a/css/style.css b/src/css/style.css similarity index 100% rename from css/style.css rename to src/css/style.css diff --git a/html/index.html b/src/html/index.html similarity index 84% rename from html/index.html rename to src/html/index.html index 7804e69..5a4b297 100644 --- a/html/index.html +++ b/src/html/index.html @@ -16,6 +16,9 @@ class="preview" /> - + + diff --git a/js/ipcMain.js b/src/ipc/main.ts similarity index 57% rename from js/ipcMain.js rename to src/ipc/main.ts index b07c953..316f372 100644 --- a/js/ipcMain.js +++ b/src/ipc/main.ts @@ -1,11 +1,11 @@ const { ipcMain } = require('electron'); -const { getImageByIndex, replaceImage } = require('./reader'); +import { getImageByIndex, replaceImage } from '../reader'; -ipcMain.on('get-image-data', (event, index) => { +ipcMain.on('get-image-data', (event: Electron.IpcMainEvent, index: number) => { event.reply('update-preview', getImageByIndex(index)); }); -ipcMain.on('replace-image', async (event, obj) => { +ipcMain.on('replace-image', async (event: Electron.IpcMainEvent, obj) => { const data = await replaceImage(obj.index, obj.path); if (data) { event.reply('update-preview', data); diff --git a/src/ipc/renderer.ts b/src/ipc/renderer.ts new file mode 100644 index 0000000..c24cc08 --- /dev/null +++ b/src/ipc/renderer.ts @@ -0,0 +1,91 @@ +//renderer process +import { ipcRenderer } from 'electron'; +import { ImageBuffer } from '../types'; + +const preview: any = document.querySelector('.preview'); +const list: any = document.querySelector('.list'); +let focused: any = null; + +if (preview) { + preview.addEventListener('drop', (event: any) => { + event.preventDefault(); + event.stopPropagation(); + + if (!focused) { + return; + } + + ipcRenderer.send('replace-image', { + index: parseInt(focused.id.split('-')[1], 10), + path: event.dataTransfer.files[0].path, + }); + }); + + preview.addEventListener('dragover', (event: any) => { + event.preventDefault(); + event.stopPropagation(); + }); +} + +ipcRenderer.on( + 'update-preview', + (event: Electron.IpcRendererEvent, data: Buffer) => { + if (preview) { + preview.src = `data:image/png;base64,${Buffer.from(data).toString( + 'base64' + )}`; + } + } +); + +ipcRenderer.on( + 'update-miniature', + (event: Electron.IpcRendererEvent, { index, data }: ImageBuffer) => { + list.querySelector( + `#btn-${index} > img` + ).src = `data:image/png;base64,${Buffer.from(data).toString('base64')}`; + } +); + +ipcRenderer.on( + 'populate-list', + (event: Electron.IpcRendererEvent, images: any[]) => { + focused = null; + + while (list.firstChild) { + list.removeChild(list.firstChild); + } + + for (const [index, image] of images.entries()) { + if (!image.isImage) { + continue; + } + + const btn = document.createElement('button'); + btn.innerText = image.name; + btn.id = `btn-${index}`; + btn.onclick = (event) => { + if (focused == event.target) { + return; + } + + if (focused) { + focused.classList.remove('focused'); + } + + focused = event.target; + focused.classList.add('focused'); + ipcRenderer.send('get-image-data', index); + }; + + const img = document.createElement('img'); + img.className = 'miniature'; + img.src = `data:image/png;base64,${Buffer.from(image.data).toString( + 'base64' + )}`; + btn.appendChild(img); + + list.appendChild(btn); + } + } +); diff --git a/rcc/rcc.exe b/src/rcc/rcc.exe similarity index 100% rename from rcc/rcc.exe rename to src/rcc/rcc.exe diff --git a/js/reader.js b/src/reader.ts similarity index 64% rename from js/reader.js rename to src/reader.ts index bf99240..590e11f 100644 --- a/js/reader.js +++ b/src/reader.ts @@ -1,31 +1,36 @@ -const { dialog, BrowserWindow, app } = require('electron'); -const path = require('path'); -const util = require('util'); +import { dialog, BrowserWindow, app } from 'electron'; +import path from 'path'; +import util from 'util'; const execFile = util.promisify(require('child_process').execFile); -const fs = require('fs-extra'); +import fs from 'fs-extra'; +import Main from './Main'; +import { Image } from './types'; -let loadedFilePath = null; -let images = []; +let loadedFilePath: string | null = null; +let images: Image[] = []; -const imageExt = ['.png', '.jpg']; +const imageExt: Array = ['.png', '.jpg']; -const resourcePath = app.isPackaged ? process.resourcesPath : '.'; +const resourcePath: string = app.isPackaged ? process.resourcesPath : __dirname; +const localPath: string = path.resolve(resourcePath, 'rcc'); const getFiles = async (path = './') => { const entries = await fs.readdir(path, { withFileTypes: true }); const files = entries - .filter((file) => !file.isDirectory()) - .map((file) => ({ ...file, path: path + file.name })); + .filter((file: any) => !file.isDirectory()) + .map((file: any) => ({ ...file, path: path + file.name })); - const folders = entries.filter((folder) => folder.isDirectory()); + const folders = entries.filter((folder: any) => folder.isDirectory()); for (const folder of folders) { files.push(...(await getFiles(`${path}/${folder.name}/`))); } return files; }; -const loadRcc = async (filePath) => { - const localPath = path.resolve(resourcePath, 'rcc'); +export async function loadRcc(filePath: string | null): Promise { + if (!filePath) { + return; + } // clear previous images images = []; @@ -42,7 +47,7 @@ const loadRcc = async (filePath) => { }); // get directory content - const files = await getFiles( + const files: any[] = await getFiles( path.join(localPath, 'qresource', 'res', 'res.rcc') ); @@ -68,9 +73,11 @@ const loadRcc = async (filePath) => { loadedFilePath = filePath; BrowserWindow.getAllWindows()[0].webContents.send('populate-list', images); -}; +} -const extractToPng = async (directoryPath) => { +export async function extractToPng( + directoryPath: string | null +): Promise { if (!images.length) { dialog.showErrorBox('Error', 'Nothing to extract.'); return; @@ -78,23 +85,26 @@ const extractToPng = async (directoryPath) => { for (const image of images) { if (image.isImage) { - await fs.outputFile(path.join(directoryPath, image.path), image.data); + await fs.outputFile( + path.join(directoryPath as string, image.path), + image.data + ); } } - dialog.showMessageBox(null, { + dialog.showMessageBox(Main.mainWindow, { message: `Png images extracted successfully. Extracted ${images.length} images.`, type: 'info', }); -}; +} -const saveRcc = async (filePath = loadedFilePath) => { - if (images.length === 0) { +export async function saveRcc( + filePath: string | null = loadedFilePath +): Promise { + if (!filePath || images.length === 0) { return; } - const localPath = path.resolve(resourcePath, 'rcc'); - // create .qrc file let data = '\n\n'; @@ -126,20 +136,25 @@ const saveRcc = async (filePath = loadedFilePath) => { } ); - await fs.move('./rcc/res/res_output.rcc', filePath, { overwrite: true }); + await fs.move(path.join(localPath, '/res/res_output.rcc'), filePath, { + overwrite: true, + }); // cleanup await fs.rmdir(path.join(localPath, 'res'), { recursive: true, }); - dialog.showMessageBox(null, { + dialog.showMessageBox(Main.mainWindow, { message: 'Rcc saved successfully.', type: 'info', }); -}; +} -const replaceImage = async (index, filePath) => { +export async function replaceImage( + index: number, + filePath: string +): Promise { const image = images[index]; if (!image) { return null; @@ -147,14 +162,8 @@ const replaceImage = async (index, filePath) => { image.data = Buffer.from(await fs.readFile(filePath, 'binary'), 'binary'); return image.data; -}; +} -const getImageByIndex = (index) => images[index]?.data; - -module.exports = { - loadRcc, - saveRcc, - replaceImage, - getImageByIndex, - extractToPng, -}; +export function getImageByIndex(index: number): Buffer { + return images[index]?.data; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2d7e192 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,11 @@ +export interface Image { + name: string; + path: string; + isImage: boolean; + data: Buffer; +} + +export interface ImageBuffer { + index: number; + data: Buffer; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..db861ec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "./build", + "noImplicitAny": true + }, + "include": ["src/**/*", "App.ts"], + "exclude": ["node_modules"] +}