├── .editorconfig ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── angular.json ├── electron-builder.json ├── main.ts ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.module.ts │ ├── home │ │ ├── comparison │ │ │ ├── comparison.component.html │ │ │ ├── comparison.component.scss │ │ │ └── comparison.component.ts │ │ ├── helper.service.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ ├── icons │ │ │ ├── icon.component.html │ │ │ ├── icon.component.ts │ │ │ ├── svg-definitions.component.html │ │ │ └── svg-definitions.component.ts │ │ ├── interfaces.ts │ │ └── quill.ts │ └── services │ │ ├── electron │ │ └── electron.service.ts │ │ └── index.ts ├── assets │ ├── favicon.icns │ ├── favicon.ico │ └── favicon.png ├── environments │ ├── environment.dev.ts │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills-test.ts ├── polyfills.ts ├── styles.scss ├── tsconfig.app.json └── typings.d.ts ├── tsconfig-serve.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "env": { 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | ], 12 | "rules": { 13 | "@typescript-eslint/consistent-type-imports": "error", 14 | "@typescript-eslint/no-unused-vars": [ 15 | "error", 16 | { 17 | "argsIgnorePattern": "_" 18 | } 19 | ], 20 | "@typescript-eslint/array-type": "error" 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [whyboris] 2 | custom: "https://paypal.me/whyboris" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | .angular 4 | 5 | # compiled output 6 | /dist 7 | /tmp 8 | /out-tsc 9 | /app-builds 10 | /release 11 | main.js 12 | src/**/*.js 13 | !src/karma.conf.js 14 | *.js.map 15 | 16 | # dependencies 17 | /node_modules 18 | 19 | # IDEs and editors 20 | /.idea 21 | .project 22 | .classpath 23 | .c9/ 24 | *.launch 25 | .settings/ 26 | *.sublime-workspace 27 | 28 | # IDE - VSCode 29 | .vscode/* 30 | .vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | 35 | # misc 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | testem.log 42 | /typings 43 | 44 | # e2e 45 | /e2e/*.js 46 | !/e2e/protractor.conf.js 47 | /e2e/*.map 48 | 49 | # System Files 50 | .DS_Store 51 | Thumbs.db 52 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "angular.ng-template", 4 | "coenraads.bracket-pair-colorizer", 5 | "editorconfig.editorconfig", 6 | "shinnn.stylelint", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at yboris@yahoo.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Boris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simplest File Renamer 2 | 3 | Rename your files directly or with your favorite text editor, making use of all your 1337 keyboard shortcuts 😉 4 | 5 | ![image](https://user-images.githubusercontent.com/17264277/69740803-0042a680-1108-11ea-9821-bc7c7f8e522d.png) 6 | 7 | ## About 8 | 9 | **Simplest File Renamer** was created by [Boris Yakubchik](https://videohubapp.com/en/about). It uses _Angular_ and _Electron_. 10 | 11 | Works on _Windows_, _Mac_, and _Linux_ :tada: 12 | 13 | ## Download 14 | 15 | The download links for all platforms are located on the app's [public webpage](https://yboris.dev/renamer/) 16 | 17 | ## License 18 | 19 | This software was built on top of [`angular-electron`](https://github.com/maximegris/angular-electron) by [Maxime GRIS](https://github.com/maximegris). It carries an [_MIT_ license](LICENSE). 20 | 21 | ## Development 22 | 23 | Main dependencies in use: 24 | 25 | | Library | Version | Date | Comment | 26 | | ---------------- | ------- | -------------- | ------- | 27 | | Angular | v18 | Jun 2024 | | 28 | | Angular-CLI | v18 | Jun 2024 | | 29 | | Electron | v31 | Jun 2024 | (internally uses Node `v20.14.0` and Chromium 124) | 30 | | Electron Builder | v24 | Jun 2024 | | 31 | 32 | Recommending Node 20 or newer. 33 | 34 | Once you install `node` and `npm` just `npm install` and `npm start` in your terminal to develop, `npm run electron` to build :wink: 35 | 36 | ## Thank you 37 | 38 | This software would not be possible without the tremendous work by other people: 39 | 40 | - [Angular](https://github.com/angular/angular) 41 | - [Electron](https://github.com/electron/electron) 42 | - [angular-electron](https://github.com/maximegris/angular-electron) 43 | - [Quill](https://github.com/quilljs/quill) 44 | - [electron-builder](https://github.com/electron-userland/electron-builder) 45 | 46 | A huge personal _thank you_ to [Percipient24](https://github.com/Percipient24) for always helping me when I ask for coding help, and for his [code](https://codepen.io/percipient24/pen/eEBOjG) that inspired this project 🙇‍♂️ 47 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "simplest-file-renamer": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets" 21 | ], 22 | "styles": [ 23 | "src/styles.scss" 24 | ], 25 | "scripts": [], 26 | "aot": false, 27 | "vendorChunk": true, 28 | "extractLicenses": false, 29 | "buildOptimizer": false, 30 | "sourceMap": true, 31 | "optimization": false, 32 | "namedChunks": true 33 | }, 34 | "configurations": { 35 | "dev": { 36 | "outputHashing": "all", 37 | "namedChunks": false, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.dev.ts" 44 | } 45 | ] 46 | }, 47 | "production": { 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "namedChunks": false, 52 | "aot": true, 53 | "extractLicenses": true, 54 | "vendorChunk": false, 55 | "buildOptimizer": true, 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.prod.ts" 60 | } 61 | ] 62 | } 63 | }, 64 | "defaultConfiguration": "" 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "buildTarget": "simplest-file-renamer:build" 70 | }, 71 | "configurations": { 72 | "dev": { 73 | "buildTarget": "simplest-file-renamer:build:dev" 74 | }, 75 | "production": { 76 | "buildTarget": "simplest-file-renamer:build:production" 77 | } 78 | } 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "buildTarget": "simplest-file-renamer:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills-test.ts", 91 | "tsConfig": "src/tsconfig.spec.json", 92 | "karmaConfig": "src/karma.conf.js", 93 | "scripts": [], 94 | "styles": [ 95 | "src/styles.scss" 96 | ], 97 | "assets": [ 98 | "src/assets", 99 | "src/favicon.ico", 100 | "src/favicon.png", 101 | "src/favicon.icns", 102 | "src/favicon.256x256.png", 103 | "src/favicon.512x512.png" 104 | ] 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-eslint/builder:lint", 109 | "options": { 110 | "eslintConfig": ".eslintrc.js", 111 | "lintFilePatterns": [ 112 | "**/*.ts" 113 | ] 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "schematics": { 120 | "@schematics/angular:component": { 121 | "prefix": "app", 122 | "style": "scss" 123 | }, 124 | "@schematics/angular:directive": { 125 | "prefix": "app" 126 | } 127 | }, 128 | "cli": { 129 | "analytics": false 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Simplest File Renamer", 3 | "appId": "com.simplestfilerenamer.simplestfilerenamer", 4 | "copyright": "Copyright © 2022 Boris Yakubchik", 5 | "directories": { 6 | "output": "release/" 7 | }, 8 | "files": [ 9 | "**/*", 10 | "!**/*.ts", 11 | "!*.code-workspace", 12 | "!LICENSE.md", 13 | "!package.json", 14 | "!package-lock.json", 15 | "!src/", 16 | "!e2e/", 17 | "!hooks/", 18 | "!angular.json", 19 | "!_config.yml", 20 | "!karma.conf.js", 21 | "!tsconfig.json" 22 | ], 23 | "win": { 24 | "icon": "src/assets/favicon", 25 | "target": [ 26 | "nsis" 27 | ] 28 | }, 29 | "mac": { 30 | "icon": "src/assets/favicon.icns", 31 | "target": [ 32 | "dmg" 33 | ] 34 | }, 35 | "linux": { 36 | "icon": "src/assets/favicon.icns", 37 | "target": [ 38 | "AppImage" 39 | ] 40 | }, 41 | "nsis": { 42 | "oneClick": false, 43 | "perMachine": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, screen, Menu } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | const settings = require('electron-settings'); 6 | 7 | let win, serve; 8 | const args = process.argv.slice(1); 9 | serve = args.some(val => val === '--serve'); 10 | 11 | interface WindowSettings { 12 | x: number; 13 | y: number; 14 | width: number; 15 | height: number; 16 | } 17 | 18 | function createWindow() { 19 | 20 | const windowSettings: WindowSettings = getWindowSettings(); 21 | 22 | console.log(windowSettings); 23 | 24 | Menu.setApplicationMenu(null); 25 | 26 | // Create the browser window. 27 | win = new BrowserWindow({ 28 | x: windowSettings.x, 29 | y: windowSettings.y, 30 | width: windowSettings.width, 31 | height: windowSettings.height, 32 | center: true, 33 | minWidth: 400, 34 | minHeight: 200, 35 | webPreferences: { 36 | nodeIntegration: true, 37 | contextIsolation: false 38 | }, 39 | }); 40 | 41 | if (serve) { 42 | require('electron-reload')(__dirname, { 43 | electron: require(`${__dirname}/node_modules/electron`) 44 | }); 45 | win.loadURL('http://localhost:4200'); 46 | } else { 47 | win.loadURL(url.format({ 48 | pathname: path.join(__dirname, 'dist/index.html'), 49 | protocol: 'file:', 50 | slashes: true 51 | })); 52 | } 53 | 54 | if (serve) { 55 | win.webContents.openDevTools(); 56 | } 57 | 58 | // Emitted when the window is closed. 59 | win.on('closed', () => { 60 | // Dereference the window object, usually you would store window 61 | // in an array if your app supports multi windows, this is the time 62 | // when you should delete the corresponding element. 63 | win = null; 64 | }); 65 | 66 | win.on('close', () => { 67 | const windowSizeAndPosition: WindowSettings = win.getBounds(); 68 | 69 | settings.setSync('app-location', windowSizeAndPosition); 70 | }); 71 | 72 | } 73 | 74 | try { 75 | 76 | // This method will be called when Electron has finished 77 | // initialization and is ready to create browser windows. 78 | // Some APIs can only be used after this event occurs. 79 | app.on('ready', createWindow); 80 | 81 | // Quit when all windows are closed. 82 | app.on('window-all-closed', () => { 83 | // On OS X it is common for applications and their menu bar 84 | // to stay active until the user quits explicitly with Cmd + Q 85 | if (process.platform !== 'darwin') { 86 | app.quit(); 87 | } 88 | }); 89 | 90 | app.on('activate', () => { 91 | // On OS X it's common to re-create a window in the app when the 92 | // dock icon is clicked and there are no other windows open. 93 | if (win === null) { 94 | createWindow(); 95 | } 96 | }); 97 | 98 | } catch (e) { 99 | // Catch Error 100 | // throw e; 101 | } 102 | 103 | // ================================================================================================= 104 | // CUSTOM STUFF 105 | // ================================================================================================= 106 | 107 | import { RenameObject, RenamedObject, RenameResult } from './src/app/home/interfaces'; 108 | 109 | const fs = require('fs'); 110 | 111 | const ipc = require('electron').ipcMain; 112 | const shell = require('electron').shell; 113 | 114 | const dialog = require('electron').dialog; 115 | 116 | let angularApp: any = null; 117 | 118 | const pathToAppData = app.getPath('appData'); 119 | 120 | 121 | ipc.on('just-started', function (event) { 122 | console.log('just started !!!'); 123 | angularApp = event; 124 | }); 125 | 126 | ipc.on('choose-file', function (event) { 127 | dialog.showOpenDialog(win, { 128 | properties: ['openFile', 'multiSelections'] 129 | }).then(result => { 130 | console.log(result); 131 | const filePath: string[] = result.filePaths; 132 | if (filePath.length) { 133 | event.sender.send('file-chosen', filePath); 134 | } 135 | }).catch(err => { 136 | console.log('choose-input: this should not happen!'); 137 | console.log(err); 138 | }); 139 | }); 140 | 141 | ipc.on('rename-these-files', function (event, filesToRename: RenameObject[]): void { 142 | console.log('renaming!!!'); 143 | console.log(filesToRename); 144 | 145 | const results: RenamedObject[] = filesToRename.map(element => renameThisFile(element)); 146 | 147 | console.log(results); 148 | angularApp.sender.send('renaming-report', results); 149 | }); 150 | 151 | 152 | function renameThisFile(file: RenameObject): RenamedObject { 153 | 154 | const renamedObject: RenamedObject = { 155 | path: file.path, 156 | filename: file.filename, 157 | extension: file.extension, 158 | newFilename: file.newFilename, 159 | result: undefined, 160 | }; 161 | 162 | if (file.filename === file.newFilename) { 163 | renamedObject.result = 'unchanged'; 164 | return renamedObject; 165 | } 166 | 167 | if (file.newFilename === undefined || file.newFilename.length === 0) { 168 | renamedObject.result = 'error'; 169 | renamedObject.error = 'empty file name'; 170 | return renamedObject; 171 | } 172 | 173 | if (file.newFilename.includes('/')) { 174 | renamedObject.result = 'error'; 175 | renamedObject.error = 'can not have "/" in filename'; 176 | return renamedObject; 177 | } 178 | 179 | const original: string = path.join(file.path, file.filename + file.extension); 180 | const newName: string = path.join(file.path, file.newFilename + file.extension); 181 | 182 | console.log('renaming file:'); 183 | console.log(original); 184 | console.log(newName); 185 | 186 | let result: RenameResult = 'renamed'; 187 | let errMsg: string; 188 | 189 | // check if already exists first 190 | if (fs.existsSync(newName)) { 191 | result = 'error'; 192 | errMsg = 'file name exists'; 193 | } else { 194 | try { 195 | fs.renameSync(original, newName); 196 | } catch (err) { 197 | result = 'error'; 198 | // console.log(err); 199 | if (err.code === 'ENOENT') { 200 | errMsg = 'source file not found'; 201 | } else { 202 | errMsg = 'unexpected error'; 203 | } 204 | } 205 | } 206 | 207 | renamedObject.result = result; 208 | 209 | if (errMsg) { 210 | renamedObject.error = errMsg; 211 | } 212 | 213 | return renamedObject; 214 | } 215 | 216 | 217 | ipc.on('open-txt-file', function (event, files: string): void { 218 | 219 | try { 220 | fs.statSync(path.join(pathToAppData, 'renamer-app')); 221 | } catch (e) { 222 | fs.mkdirSync(path.join(pathToAppData, 'renamer-app')); 223 | } 224 | 225 | const pathToTempTXT: string = path.join(pathToAppData, 'renamer-app', 'temp.txt'); 226 | console.log(pathToTempTXT); 227 | 228 | fs.writeFile(pathToTempTXT, files, 'utf8', () => { 229 | 230 | console.log('file written'); 231 | 232 | shell.openPath(pathToTempTXT); // normalize because on windows, the path sometimes is mixing `\` and `/` 233 | 234 | }); 235 | 236 | }); 237 | 238 | let lastModified: number = 0; 239 | 240 | /** 241 | * Check if file has been edited 242 | * if so, send its contents back to the app 243 | */ 244 | ipc.on('app-back-in-focus', function (event): void { 245 | 246 | const currentModified: number = fs.statSync(path.join(pathToAppData, 'renamer-app', 'temp.txt')).mtimeMs; 247 | 248 | if (lastModified === currentModified) { 249 | return; 250 | } else { 251 | lastModified = currentModified; 252 | } 253 | 254 | fs.readFile(path.join(pathToAppData, 'renamer-app', 'temp.txt'), 'utf8', (err, data) => { 255 | if (err) { 256 | console.log(err); 257 | } else { 258 | console.log('file read:'); 259 | // console.log(data); 260 | angularApp.sender.send('txt-file-updated', data); 261 | } 262 | }); 263 | 264 | }); 265 | 266 | /** 267 | * Determine app location and size based on last location 268 | * or default to something good. 269 | */ 270 | function getWindowSettings(): WindowSettings { 271 | 272 | const electronScreen = screen; 273 | 274 | const desktopSize = electronScreen.getPrimaryDisplay().workAreaSize; // { height: number, width: number } 275 | 276 | const screenWidth = desktopSize.width; 277 | const screenHeight = desktopSize.height; 278 | 279 | const defaultSize: WindowSettings = { 280 | x: desktopSize.width / 2 - 300, 281 | y: desktopSize.height / 2 - 150, 282 | width: 600, 283 | height: 300, 284 | }; 285 | 286 | const savedSettings = settings.getSync('app-location'); 287 | 288 | // Make sure the app isn't off-screen (perhaps due to monitor change) 289 | if ( savedSettings 290 | && savedSettings.x < screenWidth - 200 291 | && savedSettings.y < screenHeight - 200 292 | ) { 293 | return savedSettings; 294 | } else { 295 | return defaultSize; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplest-file-renamer", 3 | "version": "1.0.2", 4 | "description": "Simplest app to rename your files", 5 | "homepage": "https://github.com/whyboris/Simplest-File-Renamer", 6 | "author": { 7 | "name": "Boris Yakubchik", 8 | "email": "yboris@yahoo.com" 9 | }, 10 | "keywords": [ 11 | "renamer", 12 | "rename" 13 | ], 14 | "main": "main.js", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/whyboris/Simplest-File-Renamer.git" 19 | }, 20 | "scripts": { 21 | "start": "npm-run-all -p electron:serve ng:serve", 22 | "build": "npm run electron:serve-tsc && ng build --base-href ./", 23 | "build:prod": "npm run build -- -c production", 24 | "electron": "npm run build:prod && electron-builder build", 25 | "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", 26 | "electron:serve-tsc": "tsc -p tsconfig-serve.json", 27 | "ng": "ng", 28 | "ng:serve": "ng serve", 29 | "postinstall": "electron-builder install-app-deps", 30 | "lint": "ng lint" 31 | }, 32 | "dependencies": { 33 | "electron-settings": "4.0.4", 34 | "path": "0.12.7" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "18.0.3", 38 | "@angular-eslint/builder": "18.0.1", 39 | "@angular/cli": "18.0.3", 40 | "@angular/common": "18.0.2", 41 | "@angular/compiler": "18.0.2", 42 | "@angular/compiler-cli": "18.0.2", 43 | "@angular/core": "18.0.2", 44 | "@angular/forms": "18.0.2", 45 | "@angular/language-service": "18.0.2", 46 | "@angular/platform-browser": "18.0.2", 47 | "@angular/platform-browser-dynamic": "18.0.2", 48 | "@types/node": "20.14.2", 49 | "@types/quill": "2.0.14", 50 | "@typescript-eslint/eslint-plugin": "^7.13.0", 51 | "@typescript-eslint/parser": "^7.13.0", 52 | "electron": "31.0.0", 53 | "electron-builder": "24.13.3", 54 | "electron-reload": "1.5.0", 55 | "npm-run-all": "4.1.5", 56 | "quill": "1.3.7", 57 | "rxjs": "7.8.1", 58 | "ts-node": "10.9.2", 59 | "typescript": "5.4.5", 60 | "wait-on": "7.2.0", 61 | "webdriver-manager": "12.1.9", 62 | "zone.js": "0.14.7" 63 | }, 64 | "browserslist": [ 65 | "chrome 126" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import '../polyfills'; 3 | 4 | import { NgModule } from '@angular/core'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { FormsModule } from '@angular/forms'; 7 | 8 | import { HelperService } from './home/helper.service'; 9 | 10 | import { ComparisonComponent } from './home/comparison/comparison.component'; 11 | import { HomeComponent } from './home/home.component'; 12 | import { IconComponent } from './home/icons/icon.component'; 13 | import { SvgDefinitionsComponent } from './home/icons/svg-definitions.component'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | ComparisonComponent, 18 | HomeComponent, 19 | IconComponent, 20 | SvgDefinitionsComponent 21 | ], 22 | imports: [ 23 | BrowserModule, 24 | FormsModule 25 | ], 26 | providers: [ 27 | HelperService 28 | ], 29 | bootstrap: [ 30 | HomeComponent 31 | ] 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /src/app/home/comparison/comparison.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 8 | {{ icon.error }} 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/app/home/comparison/comparison.component.scss: -------------------------------------------------------------------------------- 1 | .whiteout { 2 | background: white; 3 | display: block; 4 | height: 19px; 5 | left: -100vw; 6 | min-width: calc(150vw - 9px); 7 | opacity: 0.7; 8 | pointer-events: none; 9 | position: absolute; 10 | width: calc(150vw - 9px); 11 | } 12 | 13 | .icon { 14 | display: block; 15 | width: 19px; 16 | height: 19px; 17 | } 18 | 19 | .show-error { 20 | cursor: not-allowed; 21 | 22 | .error-text { 23 | background: white; 24 | border-radius: 5px; 25 | border: 1px solid gray; 26 | display: none; 27 | padding: 5px 10px; 28 | transform: translate(-10px, -3px); 29 | position: absolute; 30 | text-align: center; 31 | white-space: nowrap; 32 | z-index: 21; 33 | } 34 | 35 | &:hover { 36 | .error-text { 37 | display: block; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/home/comparison/comparison.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import type { RenamedObject, RenameObject } from '../interfaces'; 4 | 5 | @Component({ 6 | selector: 'app-comparison', 7 | templateUrl: './comparison.component.html', 8 | styleUrls: ['./comparison.component.scss'] 9 | }) 10 | export class ComparisonComponent { 11 | 12 | @Input() files: RenamedObject[] | RenameObject[]; 13 | 14 | constructor() { } 15 | 16 | getIcon(file: RenamedObject | RenameObject): string { 17 | 18 | if (file.filename === file.newFilename) { 19 | return 'icon-equals'; 20 | } else if (file.newFilename === '' || !file.newFilename) { 21 | return 'icon-error'; 22 | } else { 23 | return 'icon-arrow'; 24 | } 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/home/helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class HelperService { 5 | 6 | constructor() { } 7 | 8 | /** 9 | * Generate Quill ops for additions 10 | * @param oldContent 11 | * @param newContent 12 | */ 13 | public find_additions(oldContent, newContent) { 14 | const diff = oldContent.diff(newContent); 15 | 16 | for (let i = 0; i < diff.ops.length; i++) { 17 | const op = diff.ops[i]; 18 | if (op.hasOwnProperty('insert')) { 19 | op.attributes = { 20 | background: '#cce8cc', // COLOR green 21 | color: '#003700' 22 | }; 23 | } 24 | } 25 | 26 | const adjusted: any = oldContent.compose(diff); 27 | 28 | return adjusted; 29 | } 30 | 31 | /** 32 | * Generate Quill ops for deletions 33 | * @param oldContent 34 | * @param newContent 35 | */ 36 | public find_deletions(oldContent, newContent) { 37 | const diff = oldContent.diff(newContent); 38 | 39 | const newOps = { 40 | ops: [] 41 | }; 42 | 43 | for (let i = 0; i < diff.ops.length; i++) { 44 | const op = diff.ops[i]; 45 | 46 | if (op.hasOwnProperty('retain')) { 47 | newOps.ops.push(op); 48 | } 49 | 50 | if (op.hasOwnProperty('delete')) { 51 | // keep the text 52 | op.retain = op.delete; 53 | delete op.delete; 54 | // but color it red and struckthrough 55 | op.attributes = { 56 | background: '#e8cccc', // COLOR red 57 | color: '#370000', 58 | strike: true 59 | }; 60 | newOps.ops.push(op); 61 | } 62 | 63 | } 64 | 65 | const adjusted = oldContent.compose(newOps); 66 | 67 | return adjusted; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 |
8 |
9 | 10 |
16 |
17 | 18 |
22 | 25 |
26 | 27 |
28 | 29 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
47 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 | Drag & Drop files here 65 |
66 | 67 | 68 | 69 | 70 | 78 | 79 | 88 | 89 | 98 | 99 | 107 | 108 | 109 | 110 | 111 |
115 | 116 |
117 | 118 | {{ finalReport.renamed }} 119 | 120 |
121 | 122 |
123 | 124 | renamed 125 | 126 |
127 | 128 |
129 | 130 | {{ finalReport.unchanged }} 131 | 132 |
133 | 134 |
135 | 136 | unchanged 137 | 138 |
139 | 140 | 141 |
142 | 143 | {{ finalReport.failed }} 144 | 145 |
146 | 147 |
148 | 149 | failed 150 | 151 |
152 |
153 | 154 | 155 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | $white: #FFFFFF; 2 | $gray-05: #F2F2F2; 3 | $gray-10: #E6E6E6; 4 | $gray-15: #d8d8d8; 5 | $gray-20: #CCCCCC; 6 | $gray-30: #B3B3B3; 7 | $gray-40: #999999; 8 | $gray-50: #808080; 9 | $gray-60: #666666; 10 | $gray-70: #4D4D4D; 11 | $gray-80: #333333; 12 | $gray-90: #1A1A1A; 13 | $black: #000000; 14 | $blue: #3278e7; 15 | $light-blue: #8db3f2; 16 | 17 | @mixin scrollBar { 18 | &::-webkit-scrollbar-track { 19 | background-color: $white; 20 | } 21 | 22 | &::-webkit-scrollbar { 23 | width: 10px; 24 | height: 10px; 25 | background-color: $white; 26 | } 27 | 28 | &::-webkit-scrollbar-thumb { 29 | background-color: $gray-30; 30 | } 31 | } 32 | 33 | button { 34 | background: $gray-15; 35 | border-radius: 10px; 36 | border: 1px solid $gray-30; 37 | bottom: 20px; 38 | cursor: pointer; 39 | font-family: monospace; 40 | height: 40px; 41 | max-height: 50px; 42 | outline: none; 43 | padding: 4px; 44 | position: absolute; 45 | transition-property: background-color, box-shadow, color; 46 | transition: 300ms; 47 | width: 40px; 48 | 49 | &:enabled { 50 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2); 51 | } 52 | 53 | &:hover { 54 | background: $gray-10; 55 | } 56 | 57 | &:disabled { 58 | cursor: initial; 59 | pointer-events: none; 60 | background: $gray-05; 61 | border-color: $gray-15; 62 | } 63 | 64 | &:active { 65 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.5); 66 | transform: translateY(1px); 67 | transition: 100ms; 68 | } 69 | } 70 | 71 | .button-left { 72 | left: 20px; 73 | } 74 | 75 | .button-middle { 76 | left: calc(50% - 20px); 77 | } 78 | 79 | .button-right { 80 | right: 20px; 81 | } 82 | 83 | ::ng-deep .ql-editor { 84 | background: white; 85 | font-family: monospace; 86 | line-height: 6px; 87 | margin: 0; 88 | outline: none; 89 | padding: 10px 0; 90 | 91 | p { 92 | white-space: nowrap; 93 | 94 | &:after { 95 | border: 1px solid white; // hack to have space at the end of the longest line when x-scrolling 96 | content: ''; 97 | position: absolute; 98 | width: 30px; 99 | } 100 | } 101 | } 102 | 103 | ::ng-deep .ql-clipboard { 104 | display: none; 105 | } 106 | 107 | .disabled { 108 | pointer-events: none; 109 | opacity: 0.5; 110 | } 111 | 112 | .rename-container { 113 | background: white; 114 | border-bottom: 1px solid $gray-30; 115 | box-sizing: border-box; 116 | height: calc(100vh - 80px); 117 | left: 0; 118 | margin: 0; 119 | overflow-x: hidden; 120 | overflow-y: hidden; 121 | padding: 0; 122 | position: absolute; 123 | width: 100vw; 124 | 125 | .ql-container { 126 | box-sizing: border-box; 127 | height: calc(100vh - 81px); 128 | left: 0; 129 | margin: 0; 130 | overflow-x: scroll; 131 | overflow-y: scroll; 132 | padding: 0; 133 | position: absolute; 134 | top: 0; 135 | width: calc(50vw - 30px); 136 | 137 | @include scrollBar; 138 | } 139 | 140 | & :nth-child(1) { 141 | overflow: hidden; // hide scrollbars 142 | padding-bottom: 10px; // compensate for hidden scrollbar 143 | transform: translateX(20px); 144 | user-select: none; 145 | } 146 | 147 | & :nth-child(2) { 148 | left: 50%; 149 | transform: translateX(30px); // compensate .ql-container.width 150 | } 151 | } 152 | 153 | .rename-overlay { 154 | pointer-events: none; 155 | user-select: none; 156 | } 157 | 158 | .compare-icons { 159 | left: calc(50vw - 8px); 160 | line-height: 19px; 161 | position: absolute; 162 | top: 17px; 163 | z-index: 4; 164 | } 165 | 166 | .fade-out { 167 | // border: 1px solid red; 168 | height: calc(100vh - 90px); 169 | pointer-events: none; 170 | position: absolute; 171 | top: 0; 172 | z-index: 2; 173 | } 174 | 175 | .fade-center-left { 176 | background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.8), white); 177 | width: 30px; 178 | right: 50vw; 179 | } 180 | 181 | .fade-right { 182 | background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.8), white); 183 | width: 30px; 184 | right: 10px; 185 | } 186 | 187 | .animated { 188 | transition-property: opacity; 189 | transition-duration: 150ms; 190 | transition-delay: 0; 191 | } 192 | 193 | .instructions { 194 | display: block; 195 | font-family: monospace; 196 | font-size: 30px; 197 | left: 50%; 198 | opacity: 0.5; 199 | pointer-events: none; 200 | position: absolute; 201 | text-align: center; 202 | top: calc(50vh - 50px); 203 | transform: translateX(-50%); 204 | user-select: none; 205 | width: 90%; 206 | z-index: 6; 207 | } 208 | 209 | .rename-report { 210 | bottom: 9px; 211 | left: calc(50vw - 8px); // coincides with comparison icons column (.compare-icons) 212 | line-height: 20px; 213 | position: absolute; 214 | 215 | .report-item { 216 | height: 20px; 217 | display: block; 218 | position: relative; 219 | 220 | .report-number { 221 | font-family: monospace; 222 | position: absolute; 223 | right: calc(100% + 10px); 224 | } 225 | 226 | .report-text { 227 | position: absolute; 228 | left: 28px; 229 | } 230 | 231 | .report-icon { 232 | width: 20px; 233 | position: absolute; 234 | top: 0; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, ViewChild } from '@angular/core'; 2 | import type { AfterViewInit, ElementRef, OnInit } from '@angular/core'; 3 | 4 | import * as path from 'path'; 5 | import * as QuillRef from './quill'; 6 | import type Quill from 'quill'; 7 | 8 | import { ElectronService } from '../services'; 9 | 10 | import { defaultOptions } from './interfaces'; 11 | import type { SourceOfTruth, RenameObject, RenamedObject } from './interfaces'; 12 | 13 | import { HelperService } from './helper.service'; 14 | 15 | @Component({ 16 | selector: 'app-root', 17 | templateUrl: './home.component.html', 18 | styleUrls: ['./home.component.scss'] 19 | }) 20 | export class HomeComponent implements AfterViewInit, OnInit { 21 | 22 | @ViewChild('editor1', { static: true }) editorNode1: ElementRef; // input file 23 | @ViewChild('editor2', { static: true }) editorNode2: ElementRef; // editable one 24 | @ViewChild('editor3', { static: true }) editorNode3: ElementRef; // input overlay (deletions) 25 | @ViewChild('editor4', { static: true }) editorNode4: ElementRef; // output overlay (additions) 26 | 27 | @ViewChild('comparison1', { static: true }) comparison1: ElementRef; // middle bar with comparison icons 28 | @ViewChild('comparison2', { static: true }) comparison2: ElementRef; // middle bar with comparison icons 29 | 30 | editor1: Quill; 31 | editor2: Quill; 32 | editor3: Quill; 33 | editor4: Quill; 34 | 35 | nodeRef1: HTMLElement; 36 | nodeRef2: HTMLElement; 37 | nodeRef3: HTMLElement; 38 | nodeRef4: HTMLElement; 39 | 40 | compare1: HTMLElement; 41 | compare2: HTMLElement; 42 | 43 | appInFocus = true; 44 | editingInTXT = false; 45 | hover = false; 46 | 47 | userUpdatedText = false; // used for deciding whether to `findDiff` on hover in/out 48 | 49 | compareIcons: RenamedObject[] | RenameObject[] = []; 50 | sourceOfTruth: SourceOfTruth[] = []; 51 | 52 | mode: 'review' | 'edit' = 'edit'; 53 | 54 | finalReport = { 55 | failed: 0, 56 | renamed: 0, 57 | unchanged: 0, 58 | }; 59 | 60 | // error modal 61 | errorModalShowing = false; 62 | numberOfSuccesses: number = 0; 63 | allErrors: any = {}; 64 | 65 | @HostListener('document:keydown', ['$event']) 66 | handleArrowKeys(event: KeyboardEvent) { 67 | // stop showing diff if user starts to navigate with arrows 68 | if (['ArrowDown','ArrowUp','ArrowLeft','ArrowRight'].includes(event.key)) { 69 | this.hover = true; 70 | } 71 | } 72 | 73 | @HostListener('document:keypress', ['$event']) 74 | handleKeyboardEvent(event: KeyboardEvent) { 75 | // if editor has selection & user is typing, hide the diff overlay 76 | if (this.mode === 'edit' && this.editor2.getSelection() !== null) { 77 | this.userUpdatedText = true; 78 | this.hover = true; 79 | } 80 | } 81 | 82 | @HostListener('window:blur', ['$event']) 83 | onBlur(event: any): void { 84 | this.appInFocus = false; 85 | } 86 | 87 | @HostListener('window:focus', ['$event']) 88 | onFocus(event: any): void { 89 | this.appInFocus = true; 90 | if (this.editingInTXT && this.mode === 'edit') { 91 | // if user is returning to window, they may have edited the txt file 92 | this.electronService.ipcRenderer.send('app-back-in-focus', undefined); 93 | } 94 | } 95 | 96 | // needs to be above `keyBindings` else maybe it doesn't work? 97 | toggler = () => { 98 | this.findDiff(); 99 | this.scrollToCorrectPositions(); 100 | this.hover = !this.hover; 101 | } 102 | 103 | // TODO - see if eslint complains 104 | // tslint:disable-next-line: member-ordering 105 | keyBindings: any = { 106 | tab: { 107 | key: 9, // Tab 108 | handler: this.toggler, 109 | }, 110 | enter: { 111 | key: 13, // Enter 112 | handler: this.toggler, 113 | }, 114 | esc: { 115 | key: 27, // Escape 116 | handler: this.toggler, 117 | } 118 | }; 119 | 120 | constructor( 121 | public electronService: ElectronService, 122 | public helperService: HelperService, 123 | ) { } 124 | 125 | ngOnInit() { 126 | 127 | // Only contains event listeners for node messages 128 | 129 | this.electronService.ipcRenderer.send('just-started'); 130 | 131 | this.electronService.ipcRenderer.on('file-chosen', (event, filePath: string[]) => { 132 | this.addToFileList(filePath.sort()); // sort alphabetically 133 | }); 134 | 135 | this.electronService.ipcRenderer.on('txt-file-updated', (event, newText: string) => { 136 | const newOps: any = { 137 | ops: [{ 138 | insert: newText 139 | }] 140 | }; 141 | this.editor2.setContents(newOps); 142 | this.findDiff(); 143 | }); 144 | 145 | this.electronService.ipcRenderer.on('renaming-report', (event, report: RenamedObject[]) => { 146 | 147 | this.mode = 'review'; 148 | 149 | this.editor1.setContents(this.editor3.getContents()); 150 | this.editor2.setContents(this.editor4.getContents()); 151 | this.editor2.disable(); // sets to `readOnly` 152 | 153 | this.compareIcons = report; 154 | 155 | this.finalReport = { 156 | failed: 0, 157 | renamed: 0, 158 | unchanged: 0, 159 | }; 160 | 161 | report.forEach(element => { 162 | switch (element.result) { 163 | case 'renamed': 164 | this.finalReport.renamed++; 165 | break; 166 | case 'unchanged': 167 | this.finalReport.unchanged++; 168 | break; 169 | case 'error': 170 | this.finalReport.failed++; 171 | break; 172 | } 173 | }); 174 | }); 175 | 176 | } 177 | 178 | // Set up to handle drag & drop of files 179 | ngAfterViewInit() { 180 | document.ondragover = document.ondrop = (ev) => { 181 | ev.preventDefault(); 182 | }; 183 | 184 | document.body.ondrop = (ev) => { 185 | if (ev.dataTransfer.files.length > 0) { 186 | const fileListObject: any = ev.dataTransfer.files; 187 | ev.preventDefault(); 188 | if (fileListObject) { 189 | 190 | const fileList: string[] = []; 191 | 192 | for (let i = 0; i < fileListObject.length; i++) { 193 | fileList.push(fileListObject[i].path); 194 | } 195 | 196 | if (this.mode === 'edit') { 197 | this.addToFileList(fileList.sort()); // sort alphabetically 198 | } 199 | } 200 | } 201 | }; 202 | 203 | // set up Quill 204 | const customOptions = defaultOptions; 205 | const readOnly = JSON.parse(JSON.stringify(defaultOptions)); 206 | readOnly.readOnly = true; 207 | customOptions.modules.keyboard.bindings = this.keyBindings; 208 | this.editor1 = new QuillRef.Quill(this.editorNode1.nativeElement, readOnly); 209 | this.editor2 = new QuillRef.Quill(this.editorNode2.nativeElement, defaultOptions); 210 | this.editor3 = new QuillRef.Quill(this.editorNode3.nativeElement, readOnly); 211 | this.editor4 = new QuillRef.Quill(this.editorNode4.nativeElement, readOnly); 212 | 213 | this.nodeRef1 = this.editorNode1.nativeElement; 214 | this.nodeRef2 = this.editorNode2.nativeElement; 215 | this.nodeRef3 = this.editorNode3.nativeElement; 216 | this.nodeRef4 = this.editorNode4.nativeElement; 217 | 218 | this.compare1 = this.comparison1.nativeElement; 219 | this.compare2 = this.comparison2.nativeElement; 220 | } 221 | 222 | /** 223 | * Add files to current list 224 | * 1) don't add unless it's a file not on the list 225 | * 2) append _input_ and _output_ with new filename 226 | * 3) reload the diff view 227 | * @param files 228 | */ 229 | addToFileList(files: string[]) { 230 | const input = this.editor1.getContents(); 231 | const output = this.editor2.getContents(); 232 | 233 | // clean up remove the `\n` at the beginning 234 | const newInput = { ops: [] }; 235 | input.ops.forEach((element) => { 236 | if (element.insert !== '\n') { // do not include the first line 237 | newInput.ops.push(element); 238 | } 239 | }); 240 | 241 | const newOutput = { ops: [] }; 242 | output.ops.forEach((element) => { 243 | if (element.insert !== '\n') { // do not include the first line 244 | newOutput.ops.push(element); 245 | } 246 | }); 247 | 248 | files.forEach((file: string) => { 249 | 250 | const currentFile = path.parse(file); 251 | 252 | if (currentFile.ext) { // only add if extension exists (else it's a folder) 253 | 254 | let fileAlreadyAdded: boolean = false; 255 | 256 | // Validate the file hasn't been added yet 257 | this.sourceOfTruth.forEach((element) => { 258 | if ( 259 | element.filename === currentFile.name 260 | && element.path === currentFile.dir 261 | && element.extension === currentFile.ext 262 | ) { 263 | fileAlreadyAdded = true; 264 | } 265 | }); 266 | 267 | if (!fileAlreadyAdded) { 268 | 269 | this.sourceOfTruth.push({ 270 | extension: currentFile.ext, 271 | filename: currentFile.name, 272 | path: currentFile.dir, 273 | }); 274 | 275 | newInput.ops.push({ insert: currentFile.name + '\n' }); 276 | newOutput.ops.push({ insert: currentFile.name + '\n' }); 277 | } 278 | } 279 | }); 280 | 281 | this.editor1.setContents(newInput); 282 | this.editor2.setContents(newOutput); 283 | 284 | this.findDiff(); 285 | } 286 | 287 | /** 288 | * Generate the deletions/additions markup and render 289 | */ 290 | findDiff() { 291 | const oldContent = this.editor1.getContents(); 292 | const newContent = this.editor2.getContents(); 293 | 294 | const deleteOnly = this.helperService.find_deletions(oldContent, newContent); 295 | const addOnly = this.helperService.find_additions(oldContent, newContent); 296 | 297 | this.editor3.setContents(deleteOnly); 298 | this.editor4.setContents(addOnly); 299 | 300 | this.userUpdatedText = false; 301 | 302 | this.compareIcons = this.getNewSourceOfTruth(); 303 | } 304 | 305 | // ======================= UI INTERRACTIONS ====================================================== 306 | 307 | 308 | updateScroll() { 309 | this.hover = true; 310 | this.nodeRef1.scrollLeft = this.nodeRef2.scrollLeft; 311 | this.nodeRef1.scrollTop = this.nodeRef2.scrollTop; 312 | 313 | this.alignComparisonColumn(); 314 | } 315 | 316 | /** 317 | * Update UI after mouse enters the text editing area 318 | */ 319 | mouseEntered() { 320 | if (this.mode === 'edit') { 321 | this.hover = true; 322 | } 323 | } 324 | 325 | /** 326 | * Update UI after mouse leaves the text editing area 327 | * 1) find the diff 328 | * 2) align the scrolling location, both X & Y 329 | */ 330 | mouseLeft() { 331 | if (this.mode === 'edit') { 332 | 333 | if (this.userUpdatedText) { 334 | this.findDiff(); 335 | } 336 | 337 | this.scrollToCorrectPositions(); 338 | 339 | this.hover = false; 340 | } 341 | } 342 | 343 | scrollToCorrectPositions() { 344 | this.nodeRef3.scrollLeft = this.nodeRef2.scrollLeft; 345 | this.nodeRef3.scrollTop = this.nodeRef2.scrollTop; 346 | 347 | this.nodeRef4.scrollLeft = this.nodeRef2.scrollLeft; 348 | this.nodeRef4.scrollTop = this.nodeRef2.scrollTop; 349 | 350 | this.alignComparisonColumn(); 351 | } 352 | 353 | /** 354 | * Adjust the center comparison column depending on editor scroll amount 355 | */ 356 | alignComparisonColumn() { 357 | const offsetStyle: string = "translateY(-" + this.nodeRef2.scrollTop + "px)"; 358 | 359 | this.compare1.style.transform = offsetStyle; 360 | this.compare2.style.transform = offsetStyle; 361 | } 362 | 363 | /** 364 | * Open system dialog for adding new file or files 365 | */ 366 | addFile() { 367 | this.electronService.ipcRenderer.send('choose-file'); 368 | } 369 | 370 | /** 371 | * Open the filenames with system's default .txt editor 372 | */ 373 | openTXT() { 374 | this.editingInTXT = true; 375 | this.electronService.ipcRenderer.send('open-txt-file', this.editor2.getText()); 376 | } 377 | 378 | /** 379 | * Start the rename process -- send data to Node 380 | * 381 | * Send the whole list to Node 382 | * Node will annotate it and return it back with `error`, `success`, or `unchanged` 383 | * 384 | */ 385 | renameStuff(): void { 386 | 387 | // todo -- lock up UI somehow !? -- while node renames stuff 388 | 389 | this.electronService.ipcRenderer.send('rename-these-files', this.getNewSourceOfTruth()); 390 | } 391 | 392 | /** 393 | * Generate new `sourceOfTruth` object with `newFilename` field 394 | */ 395 | getNewSourceOfTruth(): RenameObject[] { 396 | const fileNames = this.editor2.getText().split('\n'); 397 | fileNames.pop(); // last element always `\n' 398 | 399 | // now do renaming against `sourceOfTruth` 400 | const newSourceOfTruth: RenameObject[] = []; 401 | 402 | this.sourceOfTruth.forEach((element, index) => { 403 | newSourceOfTruth.push({ 404 | path: element.path, 405 | filename: element.filename, 406 | extension: element.extension, 407 | newFilename: fileNames[index], 408 | }); 409 | }); 410 | 411 | return newSourceOfTruth; 412 | } 413 | 414 | /** 415 | * Reset the app to ~ initial state 416 | */ 417 | restart(): void { 418 | this.sourceOfTruth = []; 419 | this.editor1.setContents([]); 420 | this.editor2.setContents([]); 421 | this.findDiff(); 422 | this.editor2.enable(); 423 | this.mode = 'edit'; 424 | } 425 | 426 | } 427 | -------------------------------------------------------------------------------- /src/app/home/icons/icon.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/home/icons/icon.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-icon', 5 | templateUrl: './icon.component.html', 6 | styleUrls: [] 7 | }) 8 | export class IconComponent { 9 | 10 | @Input() icon: string; 11 | 12 | constructor() { } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/home/icons/svg-definitions.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/app/home/icons/svg-definitions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-svg-definitions', 5 | templateUrl: './svg-definitions.component.html' 6 | }) 7 | export class SvgDefinitionsComponent {} 8 | -------------------------------------------------------------------------------- /src/app/home/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface SourceOfTruth { 2 | path: string; 3 | filename: string; 4 | extension: string; 5 | } 6 | 7 | export interface RenameObject extends SourceOfTruth { 8 | newFilename: string; 9 | } 10 | 11 | export type RenameResult = 'renamed' | 'unchanged' | 'error'; 12 | 13 | export interface RenamedObject extends RenameObject { 14 | result: RenameResult; 15 | error?: string; 16 | } 17 | 18 | export const defaultOptions = { 19 | formats: null, 20 | modules: { 21 | toolbar: null, 22 | keyboard: { 23 | bindings: undefined, 24 | } 25 | }, 26 | readOnly: false, 27 | theme: 'bubble', 28 | scrollingContainer: '#scrollSelector' 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/home/quill.ts: -------------------------------------------------------------------------------- 1 | export const Quill: any = require('quill'); 2 | 3 | export default Quill; 4 | -------------------------------------------------------------------------------- /src/app/services/electron/electron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | // If you import a module but never use any of the imported values other than as TypeScript types, 4 | // the resulting javascript file will look as if you never imported the module at all. 5 | import { ipcRenderer, webFrame } from 'electron'; 6 | import * as childProcess from 'child_process'; 7 | import * as fs from 'fs'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ElectronService { 13 | ipcRenderer: typeof ipcRenderer; 14 | webFrame: typeof webFrame; 15 | childProcess: typeof childProcess; 16 | fs: typeof fs; 17 | 18 | get isElectron() { 19 | return window && window.process && window.process.type; 20 | } 21 | 22 | constructor() { 23 | // Conditional imports 24 | if (this.isElectron) { 25 | this.ipcRenderer = window.require('electron').ipcRenderer; 26 | this.webFrame = window.require('electron').webFrame; 27 | 28 | this.childProcess = window.require('child_process'); 29 | this.fs = window.require('fs'); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './electron/electron.service'; 2 | -------------------------------------------------------------------------------- /src/assets/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whyboris/Simplest-File-Renamer/9eea2f03f826b9fed668ba97e2602786060fe384/src/assets/favicon.icns -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whyboris/Simplest-File-Renamer/9eea2f03f826b9fed668ba97e2602786060fe384/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whyboris/Simplest-File-Renamer/9eea2f03f826b9fed668ba97e2602786060fe384/src/assets/favicon.png -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `index.ts`, but if you do 3 | // `ng build --env=prod` then `index.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const AppConfig = { 7 | production: false, 8 | environment: 'DEV' 9 | }; 10 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: true, 3 | environment: 'PROD' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'LOCAL' 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simplest File Renamer 7 | 8 | 9 | 10 | 11 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { AppConfig } from './environments/environment'; 6 | 7 | if (AppConfig.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule, { 13 | preserveWhitespaces: false 14 | }) 15 | .catch(err => console.error(err)); 16 | -------------------------------------------------------------------------------- /src/polyfills-test.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es/reflect'; 2 | import 'zone.js/dist/zone'; 3 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * Web Animations `@angular/platform-browser/animations` 23 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 24 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 25 | */ 26 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 27 | 28 | /** 29 | * By default, zone.js will patch all possible macroTask and DomEvents 30 | * user can disable parts of macroTask/DomEvents patch by setting following flags 31 | * because those flags need to be set before `zone.js` being loaded, and webpack 32 | * will put import in the top of bundle, so user need to create a separate file 33 | * in this directory (for example: zone-flags.ts), and put the following flags 34 | * into that file, and then add the following code before importing zone.js. 35 | * import './zone-flags.ts'; 36 | * 37 | * The flags allowed in zone-flags.ts are listed here. 38 | * 39 | * The following flags will work for all browsers. 40 | * 41 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 42 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 43 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 44 | * 45 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 46 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 47 | * 48 | * (window as any).__Zone_enable_cross_context_check = true; 49 | * 50 | */ 51 | 52 | /*************************************************************************************************** 53 | * Zone JS is required by default for Angular itself. 54 | */ 55 | import 'zone.js'; // Included with Angular CLI. 56 | 57 | 58 | /*************************************************************************************************** 59 | * APPLICATION IMPORTS 60 | */ 61 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | background-color: #fafafa; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | 7 | interface Window { 8 | process: any; 9 | require: any; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig-serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es5", 9 | "types": [ 10 | "node" 11 | ], 12 | "lib": [ 13 | "es2017", 14 | "es2016", 15 | "es2015", 16 | "dom" 17 | ] 18 | }, 19 | "include": [ 20 | "main.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "ES2022", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2022", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "main.ts", 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------