├── .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 | 
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 |
19 |
20 |
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 |
26 |
27 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
123 |
124 | renamed
125 |
126 |
127 |
128 |
129 |
130 | {{ finalReport.unchanged }}
131 |
132 |
135 |
136 | unchanged
137 |
138 |
139 |
140 |
141 |
142 |
143 | {{ finalReport.failed }}
144 |
145 |
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 |
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 |
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 |
--------------------------------------------------------------------------------