├── .gitattributes
├── .gitignore
├── .prettierrc
├── .resources
├── check.png
├── icon.png
├── nix
│ └── bin
│ │ └── .keep
├── strings.json
└── win
│ └── bin
│ └── .keep
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
├── background.png
├── background@2x.png
├── icon.icns
└── icon.png
├── electron-webpack.json
├── package.json
├── src
├── common
│ ├── app.ts
│ ├── binaries.ts
│ ├── browser_window.ts
│ ├── env.ts
│ ├── exif_tool_processes.ts
│ ├── i18n.ts
│ ├── platform.ts
│ └── resources.ts
├── main
│ ├── app_setup.ts
│ ├── context_menu.ts
│ ├── dock.ts
│ ├── file_open.ts
│ ├── i18n.ts
│ ├── index.ts
│ ├── init.ts
│ ├── menu.ts
│ ├── menu_app.ts
│ ├── menu_app_about.ts
│ ├── menu_dock.ts
│ ├── menu_edit.ts
│ ├── menu_file.ts
│ ├── menu_file_open.ts
│ ├── menu_help.ts
│ ├── menu_item_open_url.ts
│ ├── menu_view.ts
│ ├── menu_window.ts
│ └── window_setup.ts
├── renderer
│ ├── add_files.ts
│ ├── display_exif.ts
│ ├── drag.ts
│ ├── empty_pane.ts
│ ├── exif_get.ts
│ ├── exif_remove.ts
│ ├── i18n.ts
│ ├── index.html
│ ├── index.ts
│ ├── menu_select_files.ts
│ ├── sanitize.ts
│ ├── select_files.ts
│ ├── selected_files.ts
│ ├── table_add_row.ts
│ └── table_update_row.ts
├── styles
│ ├── base.css
│ ├── card.css
│ ├── dark_mode.css
│ ├── display.css
│ ├── empty.css
│ ├── file_list.css
│ ├── icon.css
│ ├── popover.css
│ ├── tables.css
│ ├── typography.css
│ └── vars.css
└── types
│ └── node-exiftool
│ └── index.d.ts
├── static
└── icon.svg
├── tsconfig.json
├── update_exiftool.pl
└── yarn.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ELECTRON/NODE
2 | node_modules
3 | /dist
4 | .env
5 | *.tgz
6 |
7 | # MORE ELECTRON
8 | .DS_Store
9 | .env
10 | .gclient_done
11 | **/.npmrc
12 | .tags*
13 | .vs/
14 | .vscode/
15 | *.log
16 | *.pyc
17 | *.sln
18 | *.swp
19 | *.VC.db
20 | *.VC.VC.opendb
21 | *.vcxproj
22 | *.vcxproj.filters
23 | *.vcxproj.user
24 | *.xcodeproj
25 | /.idea/
26 | /dist/
27 | /external_binaries/
28 | /out/
29 | /vendor/.gclient
30 | /vendor/debian_jessie_mips64-sysroot/
31 | /vendor/debian_stretch_amd64-sysroot/
32 | /vendor/debian_stretch_arm-sysroot/
33 | /vendor/debian_stretch_arm64-sysroot/
34 | /vendor/debian_stretch_i386-sysroot/
35 | /vendor/gcc-4.8.3-d197-n64-loongson/
36 | /vendor/readme-gcc483-loongson.txt
37 | /vendor/download/
38 | /vendor/llvm-build/
39 | /vendor/llvm/
40 | /vendor/npm/
41 | /vendor/python_26/
42 | /vendor/native_mksnapshot
43 | /vendor/LICENSES.chromium.html
44 | /vendor/pyyaml
45 | node_modules/
46 | SHASUMS256.txt
47 | **/package-lock.json
48 | compile_commands.json
49 | .envrc
50 |
51 | # npm package
52 | /npm/dist
53 | /npm/path.txt
54 |
55 | .npmrc
56 |
57 | # Generated API definitions
58 | electron-api.json
59 | electron.d.ts
60 |
61 | # Spec hash calculation
62 | spec/.hash
63 |
64 | # Eslint Cache
65 | .eslintcache
66 |
67 | # Generated native addon files
68 | /spec-main/fixtures/native-addon/echo/build/
69 |
70 | # If someone runs tsc this is where stuff will end up
71 | ts-gen
72 |
73 | # Used to accelerate CI builds
74 | .depshash
75 | .depshash-target
76 |
77 | #LINUX
78 | #
79 | # NOTE! Don't add files that are generated in specific
80 | # subdirectories here. Add them in the ".gitignore" file
81 | # in that subdirectory instead.
82 | #
83 | # NOTE! Please use 'git ls-files -i --exclude-standard'
84 | # command after changing this file, to see if there are
85 | # any tracked files which get ignored after the change.
86 | #
87 | # Normal rules (sorted alphabetically)
88 | #
89 | .*
90 | *.a
91 | *.asn1.[ch]
92 | *.bin
93 | *.bz2
94 | *.c.[012]*.*
95 | *.dt.yaml
96 | *.dtb
97 | *.dtb.S
98 | *.dwo
99 | *.elf
100 | *.gcno
101 | *.gz
102 | *.i
103 | *.ko
104 | *.lex.c
105 | *.ll
106 | *.lst
107 | *.lz4
108 | *.lzma
109 | *.lzo
110 | *.mod
111 | *.mod.c
112 | *.o
113 | *.o.*
114 | *.patch
115 | *.s
116 | *.so
117 | *.so.dbg
118 | *.su
119 | *.symtypes
120 | *.tab.[ch]
121 | *.tar
122 | *.xz
123 | Module.symvers
124 | modules.builtin
125 | modules.order
126 |
127 | #
128 | # Top-level generic files
129 | #
130 | /tags
131 | /TAGS
132 | /linux
133 | /vmlinux
134 | /vmlinux.32
135 | /vmlinux-gdb.py
136 | /vmlinuz
137 | /System.map
138 | /Module.markers
139 | /modules.builtin.modinfo
140 | /modules.nsdeps
141 |
142 | #
143 | # RPM spec file (make rpm-pkg)
144 | #
145 | /*.spec
146 |
147 | #
148 | # Debian directory (make deb-pkg)
149 | #
150 | /debian/
151 |
152 | #
153 | # Snap directory (make snap-pkg)
154 | #
155 | /snap/
156 |
157 | #
158 | # tar directory (make tar*-pkg)
159 | #
160 | /tar-install/
161 |
162 | #
163 | # We don't want to ignore the following even if they are dot-files
164 | #
165 | !.clang-format
166 | !.cocciconfig
167 | !.get_maintainer.ignore
168 | !.gitattributes
169 | !.gitignore
170 | !.mailmap
171 |
172 | #
173 | # Generated include files
174 | #
175 | /include/config/
176 | /include/generated/
177 | /include/ksym/
178 | /arch/*/include/generated/
179 |
180 | # stgit generated dirs
181 | patches-*
182 |
183 | # quilt's files
184 | patches
185 | series
186 |
187 | # cscope files
188 | cscope.*
189 | ncscope.*
190 |
191 | # gnu global files
192 | GPATH
193 | GRTAGS
194 | GSYMS
195 | GTAGS
196 |
197 | # id-utils files
198 | ID
199 |
200 | *.orig
201 | *~
202 | \#*#
203 |
204 | #
205 | # Leavings from module signing
206 | #
207 | extra_certificates
208 | signing_key.pem
209 | signing_key.priv
210 | signing_key.x509
211 | x509.genkey
212 |
213 | # Kconfig presets
214 | /all.config
215 | /alldef.config
216 | /allmod.config
217 | /allno.config
218 | /allrandom.config
219 | /allyes.config
220 |
221 | # Kdevelop4
222 | *.kdev4
223 |
224 | # Clang's compilation database file
225 | /compile_commands.json
226 |
227 |
228 | # WINDOWS
229 | # Windows thumbnail cache files
230 | Thumbs.db
231 | Thumbs.db:encryptable
232 | ehthumbs.db
233 | ehthumbs_vista.db
234 |
235 | # Dump file
236 | *.stackdump
237 |
238 | # Folder config file
239 | [Dd]esktop.ini
240 |
241 | # Recycle Bin used on file shares
242 | $RECYCLE.BIN/
243 |
244 | # Windows Installer files
245 | *.cab
246 | *.msi
247 | *.msix
248 | *.msm
249 | *.msp
250 |
251 | # Windows shortcuts
252 | *.lnk
253 |
254 |
255 | # MAC
256 | # General
257 | .DS_Store
258 | .AppleDouble
259 | .LSOverride
260 |
261 | # Icon must end with two \r
262 | Icon
263 |
264 |
265 | # Thumbnails
266 | ._*
267 |
268 | # Files that might appear in the root of a volume
269 | .DocumentRevisions-V100
270 | .fseventsd
271 | .Spotlight-V100
272 | .TemporaryItems
273 | .Trashes
274 | .VolumeIcon.icns
275 | .com.apple.timemachine.donotpresent
276 |
277 | # Directories potentially created on remote AFP share
278 | .AppleDB
279 | .AppleDesktop
280 | Network Trash Folder
281 | Temporary Items
282 | .apdisk
283 |
284 | # git repo for ExifTool, used to pull down latest binary
285 | gitignore
286 | exiftool_downloads
287 | .resources/nix/bin/**
288 | .resources/win/bin/**
289 |
290 | yarn-error.log
291 |
292 |
293 | # Explicitly include the exiftool binaries
294 | # that were being hit by the .gitignore rule .*
295 | !.resources
296 | !.resources/nix/bin/.keep
297 | !.resources/win/bin/.keep
298 |
299 | # Explicitly include Travis CI configuration
300 | # that were being hit by the .gitignore rule .*
301 | !.travis.yml
302 |
303 | # Explicitly include Prettier configuration
304 | # that was being hit by the .gitignore rule .*
305 | !.prettierrc
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true
3 | }
4 |
--------------------------------------------------------------------------------
/.resources/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/.resources/check.png
--------------------------------------------------------------------------------
/.resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/.resources/icon.png
--------------------------------------------------------------------------------
/.resources/nix/bin/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/.resources/nix/bin/.keep
--------------------------------------------------------------------------------
/.resources/strings.json:
--------------------------------------------------------------------------------
1 | {
2 | "empty.title": {
3 | "en": "No files selected",
4 | "da": "Ingen filer valgt",
5 | "fr": "Aucun fichier sélectionné",
6 | "pl": "Nie wybrano plików",
7 | "ja": "ファイルが選択されていません",
8 | "es": "No hay archivos seleccionados",
9 | "de": "Keine Datei ausgewählt",
10 | "pt-BR": "Nenhum arquivo selecionado",
11 | "sk": "Nie sú vybraté žiadne súbory",
12 | "ru": "Нет выбранных файлов",
13 | "uk": "Немає вибраних файлів",
14 | "ar": "لا تُوجَدُ أيُّ ملفَّاتٍ مُحدَّدة",
15 | "nl": "Geen bestanden geselecteerd",
16 | "it": "Nessun file selezionato",
17 | "zh": "未选择文件",
18 | "tr": "Henüz dosya seçilmedi",
19 | "hr": "Nijedna datoteka nije odabrana",
20 | "hu": "Nincs kiválasztva fájl",
21 | "sv": "Inga filer valda",
22 | "ml": "ഫയലുകളൊന്നും തിരഞ്ഞെടുത്തിട്ടില്ല",
23 | "cs": "Nejsou vybrány žádné soubory",
24 | "vn": "Không có tập tin nào được chọn"
25 | },
26 | "empty.subtitle": {
27 | "en": "Drag and drop images, videos, or PDF files to automatically remove metadata.",
28 | "da": "Træk og slip billeder, videoer eller PDF filer for automatisk at fjerne metadata",
29 | "fr": "Faites glisser-déposer des images, des vidéos ou des fichiers PDF pour supprimer automatiquement les métadonnées.",
30 | "pl": "Przeciągnij i upuść zdjęcia, filmy lub pliki PDF, aby automatycznie usunąć z nich metadane.",
31 | "ja": "画像、動画、PDF をドラッグ&ドロップすると、メタデータが自動的に削除されます。",
32 | "es": "Arrastre y suelte imágenes, videos, o documentos PDF para automáticamente eliminar metadatos.",
33 | "de": "Ziehen Sie Bilder, Videos oder PDF-Dateien per Drag & Drop, um Metadaten automatisch zu entfernen.",
34 | "pt-BR": "Arraste e solte imagens, vídeos ou arquivos PDF para remover metadados automaticamente.",
35 | "sk": "Presuňte a pustite fotografie, videá alebo PDF súbory sem a automaticky z nich odstránia metadáta.",
36 | "ru": "Перетащите изображения, видео или PDF-файлы для автоматического удаления метаданных.",
37 | "uk": "Перетягніть зображення, відео або PDF-файли для автоматичного видалення метаданих.",
38 | "ar": "اِسحب وارمِ الصُّور، المقاطع المرئية أو ملفَّات البي-دي-اف لإزالة البيانات الوصفيَّة بشكل تلقائيّ.",
39 | "nl": "Sleep en plaats afbeeldingen, video's of PDF-bestanden om metagegevens automatisch te verwijderen.",
40 | "it": "Trascina e rilascia immagini, video o file PDF qui per rimuovere automaticamente i metadati.",
41 | "zh": "拖放图像,视频或PDF文件以自动删除元数据。",
42 | "tr": "Meta verileri otomatik olarak kaldırmak için görüntü, video veya PDF dosyalarını sürükleyip bırakabilirsin.",
43 | "hr": "Povuci i ispusti slike, videa ili PDF datoteke za automatsko uklanjanje metapodataka.",
44 | "hu": "Húzd át a képeket, videókat vagy PDF fájlokat a metaadatatok automatikus eltávolításához.",
45 | "sv": "Dra och släpp bilder, videor eller PDF filer för att ta bort metadata automatiskt",
46 | "ml": "മെറ്റാഡാറ്റ സ്വയമേവ നീക്കം ചെയ്യാൻ ചിത്രങ്ങൾ, വീഡിയോകൾ അല്ലെങ്കിൽ PDF ഫയലുകൾ വലിച്ചിടുക.",
47 | "cs": "Přetáhněte sem fotografie, videa nebo soubory PDF a automaticky z nich odstraníte metadata.",
48 | "vn": "Kéo và thả tập tin hình ảnh, video hoặc PDF để tự động xóa siêu dữ liệu."
49 | },
50 |
51 | "table.header.filename": {
52 | "en": "Selected files",
53 | "da": "Valgte filer",
54 | "fr": "Fichiers sélectionnés",
55 | "pl": "Wybrane pliki",
56 | "ja": "選択されたファイル",
57 | "es": "Archivos seleccionados",
58 | "de": "Ausgewählte Dateien",
59 | "pt-BR": "Arquivos selecionados",
60 | "sk": "Vybrané súbory",
61 | "ru": "Выбранные файлы",
62 | "uk": "Вибрані файли",
63 | "ar": "الملفَّات المُحدَّدة",
64 | "nl": "Geselecteerde bestanden",
65 | "it": "File selezionati",
66 | "zh": "选中的文件",
67 | "tr": "Seçilen dosyalar",
68 | "hr": "Odabrane datoteke",
69 | "hu": "Kiválasztott fájlok",
70 | "sv": "valda filer",
71 | "ml": "തിരഞ്ഞെടുത്ത ഫയലുകൾ",
72 | "cs": "Vybrané soubory",
73 | "vn": "Những tập tin đã được chọn"
74 | },
75 | "table.header.exif-before": {
76 | "en": "# Exif Before",
77 | "da": "Exif Før",
78 | "fr": "# Exif Avant",
79 | "pl": "# Exif przed",
80 | "ja": "# 処理前の Exif",
81 | "es": "Exif antes",
82 | "de": "# Exif vorher",
83 | "pt-BR": "# Exif antes",
84 | "sk": "# Exif predtým",
85 | "ru": "# Exif до",
86 | "uk": "# Exif до",
87 | "ar": "# قَبل Exif",
88 | "nl": "# Exif Voor",
89 | "it": "Nº di dati Exif prima",
90 | "zh": "# Exif 处理前",
91 | "tr": "# Exif'den önce",
92 | "hr": "Broj Exifa prije",
93 | "hu": "# Exif előtt",
94 | "sv": "# Exif Innan",
95 | "ml": "എക്സിഫ് മുമ്പ്",
96 | "cs": "Exif předtím",
97 | "vn": "#Trước Exif"
98 | },
99 | "table.header.exif-after": {
100 | "en": "# Exif After",
101 | "da": "Exif Efter",
102 | "fr": "# Exif Après",
103 | "pl": "# Exif po",
104 | "ja": "# 処理後の Exif",
105 | "es": "Exif después",
106 | "de": "# Exif nachher",
107 | "pt-BR": "# Exif depois",
108 | "sk": "# Exif po",
109 | "ru": "# Exif после",
110 | "uk": "# Exif після",
111 | "ar": "# بَعد Exif",
112 | "nl": "# Exif Na",
113 | "it": "Nº di dati Exif dopo",
114 | "zh": "# Exif 处理后",
115 | "tr": "# Exif'den Sonra",
116 | "hr": "Broj Exifa poslije",
117 | "hu": "# Exif után",
118 | "sv": "# Exif Efter",
119 | "ml": "എക്സിഫ് പിന്നീട്",
120 | "cs": "Exif poté",
121 | "vn": "#Sau Exif"
122 | },
123 |
124 | "contextmenu.copy": {
125 | "en": "Copy",
126 | "da": "Kopier",
127 | "fr": "Copier",
128 | "pl": "Kopiuj",
129 | "ja": "コピー",
130 | "es": "Copiar",
131 | "de": "Kopie",
132 | "pt-BR": "Copiar",
133 | "sk": "Kopírovať",
134 | "ru": "Скопировать",
135 | "uk": "Скопіювати",
136 | "ar": "نَسخ",
137 | "nl": "Kopieer",
138 | "it": "Copia",
139 | "zh": "拷贝",
140 | "tr": "Kopyala",
141 | "hr": "Kopiraj",
142 | "hu": "Másolat",
143 | "sv": "Kopiera",
144 | "ml": "പകർത്തുക",
145 | "cs": "Kopírovat",
146 | "vn": "Sao chép"
147 | },
148 | "contextmenu.select-all": {
149 | "en": "Select All",
150 | "da": "Vælg Alle",
151 | "fr": "Tout sélectionner",
152 | "pl": "Zaznacz wszystko",
153 | "ja": "全て選択",
154 | "es": "Seleccionar todos",
155 | "de": "Alle markieren",
156 | "pt-BR": "Selecionar todos",
157 | "sk": "Vybrať všetko",
158 | "ru": "Выделить всё",
159 | "uk": "Виділити все",
160 | "ar": "تحديدُ الكُل",
161 | "nl": "Alles Selecteren",
162 | "it": "Seleziona tutto",
163 | "zh": "全选",
164 | "tr": "Hepsini seç",
165 | "hr": "Odaberi sve",
166 | "hu": "Összes kijelölés",
167 | "sv": "Välj Alla",
168 | "ml": "എല്ലാം തിരഞ്ഞെടുക്കൂ",
169 | "cs": "Vybrat vše",
170 | "vn": "Chọn tất cả"
171 | },
172 |
173 | "menu.app.about": {
174 | "en": "About ",
175 | "da": "Om ",
176 | "fr": "À propos d'",
177 | "pl": "O programie ",
178 | "ja": "概要 ",
179 | "es": "Acerca de ",
180 | "de": "Über ",
181 | "pt-BR": "Sobre ",
182 | "sk": "O aplikácii ",
183 | "ru": "О программе ",
184 | "uk": "Про програму ",
185 | "ar": " حَول ",
186 | "nl": "Over ",
187 | "it": "Chi siamo",
188 | "zh": "关于",
189 | "tr": "Hakkında",
190 | "hr": "Informacije",
191 | "hu": "Névjegy",
192 | "sv": "Om",
193 | "ml": "കുറിച്ച്",
194 | "cs": "O aplikaci ",
195 | "vn": "Giới thiệu"
196 | },
197 | "menu.app.services": {
198 | "en": "Services",
199 | "da": "Services",
200 | "fr": "Services",
201 | "pl": "Usługi",
202 | "ja": "サービス",
203 | "es": "Servicios",
204 | "de": "Services",
205 | "pt-BR": "Serviços",
206 | "sk": "Služby",
207 | "ru": "Службы",
208 | "uk": "Служби",
209 | "ar": "الخدمات",
210 | "nl": "Services",
211 | "it": "Servizi",
212 | "zh": "服务",
213 | "tr": "Servisler",
214 | "hr": "Usluge",
215 | "hu": "Szolgáltatások",
216 | "sv": "Tjänster",
217 | "ml": "സർവീസുകൾ",
218 | "cs": "Služby",
219 | "vn": "Dịch vụ"
220 | },
221 | "menu.app.hide": {
222 | "en": "Hide",
223 | "da": "Skjul",
224 | "fr": "Masquer",
225 | "pl": "Ukryj",
226 | "ja": "最小化",
227 | "es": "Ocultar",
228 | "de": "Ausblenden",
229 | "pt-BR": "Ocultar",
230 | "sk": "Skryť",
231 | "ru": "Скрыть",
232 | "uk": "Сховати",
233 | "ar": "إخفاء",
234 | "nl": "Verbergen",
235 | "it": "Nascondi",
236 | "zh": "最小化",
237 | "tr": "Gizle",
238 | "hr": "Sakrij",
239 | "hu": "Elrejt",
240 | "sv": "Göm",
241 | "ml": "മറയ്ക്കുക",
242 | "cs": "Skrýt",
243 | "vn": "Ẩn"
244 | },
245 | "menu.app.hide-others": {
246 | "en": "Hide Others",
247 | "da": "Skjul Andre",
248 | "fr": "Masquer les autres",
249 | "pl": "Ukryj pozostałe",
250 | "ja": "他のウィンドウを最小化",
251 | "es": "Ocultar otros",
252 | "de": "Andere ausblenden",
253 | "pt-BR": "Ocultar outros",
254 | "sk": "Skryť ostatné",
255 | "ru": "Скрыть остальные",
256 | "uk": "Сховати решту",
257 | "ar": "إخفاء البقيَّة",
258 | "nl": "Verbergen Andere",
259 | "it": "Nascondi tutto il resto",
260 | "zh": "隐藏其他",
261 | "tr": "Diğerlerini gizle",
262 | "hr": "Sakrij ostalo",
263 | "hu": "Többi elrejtése",
264 | "sv": "Göm Andra",
265 | "ml": "മറ്റ് ഇനങ്ങൾ മറയ്ക്കുക",
266 | "cs": "Skrýt ostatní",
267 | "vn": "Ẩn những tập tin khác"
268 | },
269 | "menu.app.show-all": {
270 | "en": "Show All",
271 | "da": "Vis Alle",
272 | "fr": "Tout afficher",
273 | "pl": "Pokaż wszystkie",
274 | "ja": "全て表示",
275 | "es": "Mostrar todos",
276 | "de": "Alle anzeigen",
277 | "pt-BR": "Mostrar todos",
278 | "sk": "Ukázať všetko",
279 | "ru": "Показать все",
280 | "uk": "Показати все",
281 | "ar": "إظهار الكُل",
282 | "nl": "Toon Alles",
283 | "it": "Mostra tutti",
284 | "zh": "全部显示",
285 | "tr": "Hepsini göster",
286 | "hr": "Pokaži sve",
287 | "hu": "Összes mutatása",
288 | "sv": "Visa Alla",
289 | "ml": "എല്ലാം കാണിക്കൂ",
290 | "cs": "Zobrazit vše",
291 | "vn": "Hiện tất cả"
292 | },
293 | "menu.app.quit": {
294 | "en": "Quit",
295 | "da": "Afslut",
296 | "fr": "Quitter",
297 | "pl": "Zakończ",
298 | "ja": "終了",
299 | "es": "Salir de",
300 | "de": "Beenden",
301 | "pt-BR": "Sair",
302 | "sk": "Ukončiť",
303 | "ru": "Завершить",
304 | "uk": "Завершити",
305 | "ar": "خُروج",
306 | "nl": "Afsluiten",
307 | "it": "Esci",
308 | "zh": "退出",
309 | "tr": "Çık",
310 | "hr": "Zatvori program",
311 | "hu": "Kilépés",
312 | "sv": "Avsluta",
313 | "ml": "അടയ്ക്കുക",
314 | "cs": "Ukončit",
315 | "vn": "Thoát"
316 | },
317 |
318 | "menu.file.name": {
319 | "en": "File",
320 | "da": "Fil",
321 | "fr": "Fichier",
322 | "pl": "Plik",
323 | "ja": "ファイル",
324 | "es": "Archivo",
325 | "de": "Datei",
326 | "pt-BR": "Arquivo",
327 | "sk": "Súbor",
328 | "ru": "Файл",
329 | "uk": "Файл",
330 | "ar": "اسم الملفّ",
331 | "nl": "Bestand",
332 | "it": "File",
333 | "zh": "文件",
334 | "tr": "Dosya",
335 | "hr": "Datoteka",
336 | "hu": "Fájl",
337 | "sv": "Fil",
338 | "ml": "ഫയൽ",
339 | "cs": "Soubor",
340 | "vn": "Tập tin"
341 | },
342 | "menu.file.open": {
343 | "en": "Open",
344 | "da": "Åbn",
345 | "fr": "Ouvrir",
346 | "pl": "Otwórz",
347 | "ja": "開く",
348 | "es": "Abrir",
349 | "de": "Öffnen",
350 | "pt-BR": "Abrir arquivo",
351 | "sk": "Otvoriť",
352 | "ru": "Открыть",
353 | "uk": "Відкрити",
354 | "ar": "فَتح",
355 | "nl": "Openen",
356 | "it": "Apri",
357 | "zh": "打开",
358 | "tr": "Aç",
359 | "hr": "Otvori",
360 | "hu": "Megnyitás",
361 | "sv": "Öppna",
362 | "ml": "തുറക്കുക",
363 | "cs": "Otevřít",
364 | "vn": "Mở để chọn file"
365 | },
366 | "menu.file.close": {
367 | "en": "Close",
368 | "da": "Luk",
369 | "fr": "Fermer",
370 | "pl": "Zamknij",
371 | "ja": "閉じる",
372 | "es": "Cerrar",
373 | "de": "Schliesen",
374 | "pt-BR": "Fechar",
375 | "sk": "Zavrieť",
376 | "ru": "Закрыть",
377 | "uk": "Закрити",
378 | "ar": "إغلاق",
379 | "nl": "Sluiten",
380 | "it": "Chiudi",
381 | "zh": "关闭",
382 | "tr": "Kapat",
383 | "hr": "Zatvori",
384 | "hu": "Bezárás",
385 | "sv": "Stäng",
386 | "ml": "അടയ്ക്കുക",
387 | "cs": "Zavřít",
388 | "vn": "Đóng"
389 | },
390 | "menu.file.quit": {
391 | "en": "Quit",
392 | "da": "Afslut",
393 | "fr": "Quitter",
394 | "pl": "Zakończ",
395 | "ja": "終了",
396 | "es": "Salir",
397 | "de": "Beenden",
398 | "pt-BR": "Sair",
399 | "sk": "Ukončiť",
400 | "ru": "Завершить",
401 | "uk": "Вийти",
402 | "ar": "خُروج",
403 | "nl": "Afsluiten",
404 | "it": "Esci",
405 | "zh": "退出",
406 | "tr": "Çık",
407 | "hr": "Zatvori program",
408 | "hu": "Kilépés",
409 | "sv": "Avsluta",
410 | "ml": "അടയ്ക്കുക",
411 | "cs": "Ukončit",
412 | "vn": "Thoát"
413 | },
414 |
415 | "menu.edit.name": {
416 | "en": "Edit",
417 | "da": "Rediger",
418 | "fr": "Édition",
419 | "pl": "Edytuj",
420 | "ja": "編集",
421 | "es": "Editar",
422 | "de": "Editieren",
423 | "pt-BR": "Editar nome",
424 | "sk": "Upraviť",
425 | "ru": "Правка",
426 | "uk": "Редагування",
427 | "ar": "تحرير",
428 | "nl": "Bewerken",
429 | "it": "Modifica",
430 | "zh": "编辑",
431 | "tr": "Düzenle",
432 | "hr": "Uredi",
433 | "hu": "Szerkesztés",
434 | "sv": "Redigera",
435 | "ml": "തിരുത്തുക",
436 | "cs": "Upravit",
437 | "vn": "Chỉnh sửa"
438 | },
439 | "menu.edit.copy": {
440 | "en": "Copy",
441 | "da": "Kopier",
442 | "fr": "Copier",
443 | "pl": "Kopiuj",
444 | "ja": "コピー",
445 | "es": "Copiar",
446 | "de": "Kopieren",
447 | "pt-BR": "Copiar",
448 | "sk": "Kopírovať",
449 | "ru": "Скопировать",
450 | "uk": "Скопіювати",
451 | "ar": "نَسخ",
452 | "nl": "Kopiëren",
453 | "it": "Copia",
454 | "zh": "拷贝",
455 | "tr": "Kopyala",
456 | "hr": "Kopiraj",
457 | "hu": "Másolás",
458 | "sv": "Kopiera",
459 | "ml": "പകർത്തുക",
460 | "cs": "Kopírovat",
461 | "vn": "Sao chép"
462 | },
463 | "menu.edit.select-all": {
464 | "en": "Select All",
465 | "da": "Vælg Alle",
466 | "fr": "Tout sélectionner",
467 | "pl": "Zaznacz wszystko",
468 | "ja": "全て選択",
469 | "es": "Seleccionar todos",
470 | "de": "Alle markieren",
471 | "pt-BR": "Selecionar todos",
472 | "sk": "Vybrať všetko",
473 | "ru": "Выбрать все",
474 | "uk": "Вибрати все",
475 | "ar": "تحديدُ الكُل",
476 | "nl": "Alles Selecteren",
477 | "it": "Seleziona tutto",
478 | "zh": "全选",
479 | "tr": "Hepsini seç",
480 | "hr": "Odaberi sve",
481 | "hu": "Összes kijelölés",
482 | "sv": "Välj Alla",
483 | "ml": "എല്ലാം തിരഞ്ഞെടുക്കൂ",
484 | "cs": "Vybrat vše",
485 | "vn": "Chọn tất cả"
486 | },
487 | "menu.edit.speech": {
488 | "en": "Speech",
489 | "da": "Diktér",
490 | "fr": "Dictée",
491 | "pl": "Mowa",
492 | "ja": "スピーチ",
493 | "es": "Dictado",
494 | "de": "Sprachausgabe",
495 | "pt-BR": "Discurso",
496 | "sk": "Reč",
497 | "ru": "Проговаривание текста",
498 | "uk": "Читання вголос",
499 | "ar": "النُّطق",
500 | "nl": "Spraak",
501 | "it": "Pronuncia",
502 | "zh": "语音",
503 | "tr": "Konuş",
504 | "hr": "Govor",
505 | "hu": "Beszéd",
506 | "sv": "Diktera",
507 | "ml": "വാചകം",
508 | "cs": "Řeč",
509 | "vn": "Ngôn ngữ"
510 | },
511 | "menu.edit.speech.start-speaking": {
512 | "en": "Start Speaking",
513 | "da": "Start Diktéring",
514 | "fr": "Commencer la dictée",
515 | "pl": "Rozpocznij mówienie",
516 | "ja": "話し始める",
517 | "es": "Comience a hablar",
518 | "de": "Sprachausgabe starten",
519 | "pt-BR": "Comece a falar",
520 | "sk": "Začnite rozprávať",
521 | "ru": "Начать",
522 | "uk": "Розпочати читання",
523 | "ar": "اِبدأ النُّطق",
524 | "nl": "Start Spraakuitvoer",
525 | "it": "Inizia a parlare",
526 | "zh": "开始说话",
527 | "tr": "Konuşmaya Başla",
528 | "hr": "Pokreni govor",
529 | "hu": "Beszéd elkezdése",
530 | "sv": "Börja Prata",
531 | "ml": "വാചകം തുടങ്ങുക",
532 | "cs": "Začněte mluvit",
533 | "vn": "Bắt đầu nói"
534 | },
535 | "menu.edit.speech.stop-speaking": {
536 | "en": "Stop Speaking",
537 | "da": "Stop Diktéring",
538 | "fr": "Arrêter la dictée",
539 | "pl": "Zakończ mówienie",
540 | "ja": "話し終わる",
541 | "es": "Pare de hablar",
542 | "de": "Sprachausgabe stoppen",
543 | "pt-BR": "Parar de falar",
544 | "sk": "Prestaňte hovoriť",
545 | "ru": "Остановить",
546 | "uk": "Зупинити читання",
547 | "ar": "إيقاف النُّطق",
548 | "nl": "Stop Spraakuitvoer",
549 | "it": "Smetti di parlare",
550 | "zh": "停止说话",
551 | "tr": "Konuşmayı Bitir",
552 | "hr": "Prekini govor",
553 | "hu": "Beszéd befejezése",
554 | "sv": "Sluta Prata",
555 | "ml": "വാചകം നിർത്തുക",
556 | "cs": "Přestaňte mluvit",
557 | "vn": "Ngừng nói"
558 | },
559 |
560 | "menu.view.name": {
561 | "en": "View",
562 | "da": "Vis",
563 | "fr": "Affichage",
564 | "pl": "Widok",
565 | "ja": "表示",
566 | "es": "Vista",
567 | "de": "Anzeigen",
568 | "pt-BR": "Ver",
569 | "sk": "Zobrazenie",
570 | "ru": "Вид",
571 | "uk": "Перегляд",
572 | "ar": "العَرض",
573 | "nl": "Beeld",
574 | "it": "Visualizza",
575 | "zh": "视图",
576 | "tr": "Görünüm",
577 | "hr": "Prikaz",
578 | "hu": "Nézet",
579 | "sv": "Visa",
580 | "ml": "ദൃശ്യത",
581 | "cs": "Zobrazení",
582 | "vn": "Xem thêm"
583 | },
584 | "menu.view.toggle-dev-tools": {
585 | "en": "Toggle Developer Tools",
586 | "da": "Vis Udvikler Værktøjer",
587 | "fr": "Outils de développment",
588 | "pl": "Przełącz narzędzia developerskie",
589 | "ja": "開発者ツールの切り替え",
590 | "es": "Habilitar herramientas de desarrollador",
591 | "de": "Entwicklertools aktivieren",
592 | "pt-BR": "Ativar modo do desenvolvedor",
593 | "sk": "Prepnúť vývojárske nástroje",
594 | "ru": "Переключить инструменты разработчика",
595 | "uk": "Переключити інструменти розробника",
596 | "ar": "تبديل أدوات المُطورين",
597 | "nl": "Open Developer Tools",
598 | "it": "Apri/chiudi gli strumenti per sviluppatori",
599 | "zh": "启动开发者工具",
600 | "tr": "Geliştirici Araçlarını Aç/Kapat",
601 | "hr": "Uklj/Isklj programerske alate",
602 | "hu": "Fejlesztői eszközök",
603 | "sv": "Växla utvecklarverktyg",
604 | "ml": "ഡെവലപ്പർ ടൂളുകൾ മാറ്റുക",
605 | "cs": "Přepnout vývojářské nástroje",
606 | "vn": "Bật/Tắt công cụ dành cho nhà phát triển"
607 | },
608 | "menu.view.zoom-reset": {
609 | "en": "Actual Size",
610 | "da": "Virkelig Størrelse",
611 | "fr": "Zoom par défaut",
612 | "pl": "Domyślny rozmiar",
613 | "ja": "実サイズ",
614 | "es": "Tamaño actual",
615 | "de": "Originalgröße",
616 | "pt-BR": "Tamanho atual",
617 | "sk": "Predvolená veľkosť",
618 | "ru": "Исходный размер",
619 | "uk": "Справжній розмір",
620 | "ar": "الحجم الأصلي",
621 | "nl": "Originele grootte",
622 | "it": "Reimposta la dimensione originale",
623 | "zh": "实际大小",
624 | "tr": "Gerçek boyut",
625 | "hr": "Stvarna veličina",
626 | "hu": "Valódi méret",
627 | "sv": "Riktig Storlek",
628 | "ml": "യഥാർത്ഥ വലുപ്പം",
629 | "cs": "Výchozí velikost",
630 | "vn": "Kích cỡ thực sự"
631 | },
632 | "menu.view.zoom-in": {
633 | "en": "Zoom In",
634 | "da": "Zoom Ind",
635 | "fr": "Zoom avant",
636 | "pl": "Powiększ",
637 | "ja": "ズームイン",
638 | "es": "Acercar",
639 | "de": "Vergrößern",
640 | "pt-BR": "Ampliar",
641 | "sk": "Priblížiť",
642 | "ru": "Увеличить масштаб",
643 | "uk": "Збільшити",
644 | "ar": "تكبير",
645 | "nl": "Inzoomen",
646 | "it": "Aumenta zoom",
647 | "zh": "放大",
648 | "tr": "Yakınlaştır",
649 | "hr": "Povećaj",
650 | "hu": "Nagyítás",
651 | "sv": "Zooma in",
652 | "ml": "വലുതാക്കുക",
653 | "cs": "Přiblížit",
654 | "vn": "Phóng to màn hình chính"
655 | },
656 | "menu.view.zoom-out": {
657 | "en": "Zoom Out",
658 | "da": "Zoom Ud",
659 | "fr": "Zoom arrière",
660 | "pl": "Pomniejsz",
661 | "ja": "ズームアウト",
662 | "es": "Alejar",
663 | "de": "Verkleinern",
664 | "pt-BR": "Reduzir",
665 | "sk": "Oddialiť",
666 | "ru": "Уменьшить масштаб",
667 | "uk": "Зменьшити",
668 | "ar": "تصغير",
669 | "nl": "Uitzoomen",
670 | "it": "Diminuisci zoom",
671 | "zh": "缩小",
672 | "tr": "Uzaklaştır",
673 | "hr": "Smanji",
674 | "hu": "Kicsinyítés",
675 | "sv": "Zooma ut",
676 | "ml": "ചെറുതാക്കുക",
677 | "cs": "Oddálit",
678 | "vn": "Thu nhỏ màn hình chính"
679 | },
680 | "menu.view.toggle-full-screen": {
681 | "en": "Toggle Full Screen",
682 | "da": "Vis Fuld Skærm",
683 | "fr": "Activer le mode plein écran",
684 | "pl": "Przełącz tryb pełnoekranowy",
685 | "ja": "フルスクリーンの切り替え",
686 | "es": "Activar el modo pantalla completa",
687 | "de": "Vollbildmodus",
688 | "pt-BR": "Tela cheia",
689 | "sk": "Prepnúť režim celej obrazovky",
690 | "ru": "Перейти в полноэкранный режим",
691 | "uk": "Увійти до повноекранного режиму",
692 | "ar": "تبديل ملء الشَّاشة",
693 | "nl": "Volledig Scherm",
694 | "it": "Attiva/disattiva la modalità a schermo intero",
695 | "zh": "进入全屏幕",
696 | "tr": "Tam ekran Aç/Kapa",
697 | "hr": "Uklj/Isklj prikaz preko cijelog ekrana",
698 | "hu": "Váltás teljes képernyőre",
699 | "sv": "Växla Fullskärm",
700 | "ml": "ഫുൾ സ്ക്രീൻ മാറ്റുക",
701 | "cs": "Přepnout režim celé obrazovky",
702 | "vn": "Toàn màn hình"
703 | },
704 |
705 | "menu.window.name": {
706 | "en": "Window",
707 | "da": "Vindue",
708 | "fr": "Fenêtre",
709 | "pl": "Okno",
710 | "ja": "ウィンドウ",
711 | "es": "Ventana",
712 | "de": "Fenster",
713 | "pt-BR": "Janela",
714 | "sk": "Okno",
715 | "ru": "Окно",
716 | "uk": "Вікно",
717 | "ar": "النَّافِذة",
718 | "nl": "Venster",
719 | "it": "Finestra",
720 | "zh": "窗口",
721 | "tr": "Pencere",
722 | "hr": "Prozor",
723 | "hu": "Ablak",
724 | "sv": "Fönster",
725 | "ml": "ജാലകം",
726 | "cs": "Okno",
727 | "vn": "Màn cửa sổ"
728 | },
729 | "menu.window.minimize-mac": {
730 | "en": "Minimize",
731 | "da": "Minimér",
732 | "fr": "Réduire",
733 | "pl": "Minimalizuj",
734 | "ja": "最小化",
735 | "es": "Minimizar",
736 | "de": "Im Dock ablegen",
737 | "pt-BR": "Minimizar",
738 | "sk": "Minimalizovať",
739 | "ru": "Убрать в Dock",
740 | "uk": "Згорнути",
741 | "ar": "تقليص",
742 | "nl": "Minimaliseren",
743 | "it": "Minimizza",
744 | "zh": "最小化",
745 | "tr": "Küçült",
746 | "hr": "Smanji",
747 | "hu": "Minimalizálás",
748 | "sv": "Förringa",
749 | "ml": "മിനിമൈസ്",
750 | "cs": "Minimalizovat",
751 | "vn": "Thu nhỏ"
752 | },
753 | "menu.window.minimize": {
754 | "en": "Minimize",
755 | "da": "Minimér",
756 | "fr": "Réduire",
757 | "pl": "Minimalizuj",
758 | "ja": "最小化",
759 | "es": "Minimizar",
760 | "de": "Minimieren",
761 | "pt-BR": "Minimizar",
762 | "sk": "Minimalizovať",
763 | "ru": "Свернуть",
764 | "uk": "Згорнути",
765 | "ar": "تقليص",
766 | "nl": "Minimaliseren",
767 | "it": "Minimizza",
768 | "zh": "最小化",
769 | "tr": "Küçült",
770 | "hr": "Smanji",
771 | "hu": "Minimalizálás",
772 | "sv": "Förringa",
773 | "ml": "മിനിമൈസ്",
774 | "cs": "Minimalizovat",
775 | "vn": "Thu nhỏ"
776 | },
777 | "menu.window.zoom-mac": {
778 | "en": "Zoom",
779 | "da": "Zoom",
780 | "fr": "Réduire/Agrandir",
781 | "pl": "Zoom",
782 | "ja": "ズーム",
783 | "es": "Maximizar",
784 | "de": "Zoomen",
785 | "pt-BR": "Ampliar",
786 | "sk": "Zväčšiť",
787 | "ru": "Изменить масштаб",
788 | "uk": "Змінити масштаб",
789 | "ar": "تكبير",
790 | "nl": "Zoomen",
791 | "it": "Riduci/Ingrandisci",
792 | "zh": "缩放",
793 | "tr": "Yakınlaştır",
794 | "hr": "Zumiraj",
795 | "hu": "Zoomolás",
796 | "sv": "Zooma",
797 | "ml": "സൂം ചെയ്യുക",
798 | "cs": "Zvětšit",
799 | "vn": "Phóng to"
800 | },
801 | "menu.window.zoom": {
802 | "en": "Zoom",
803 | "da": "Zoom",
804 | "fr": "Agrandir",
805 | "pl": "Zoom",
806 | "ja": "ズーム",
807 | "es": "Maximizar",
808 | "de": "Zoomen",
809 | "pt-BR": "Ampliar",
810 | "sk": "Zväčšiť",
811 | "ru": "Развернуть",
812 | "uk": "Розгорнути",
813 | "ar": "تكبير",
814 | "nl": "Zoomen",
815 | "it": "Massimizza",
816 | "zh": "缩放",
817 | "tr": "Yakınlaştır",
818 | "hr": "Zumiraj",
819 | "hu": "Zoomolás",
820 | "sv": "Zooma",
821 | "ml": "സൂം ചെയ്യുക",
822 | "cs": "Zvětšit",
823 | "vn": "Phóng to"
824 | },
825 | "menu.window.front": {
826 | "en": "Bring All to Front",
827 | "da": "Bring Alle I Front",
828 | "fr": "Tout ramener au premier plan",
829 | "pl": "Umieść wszystko na wierzchu",
830 | "ja": "全てを前面に表示",
831 | "es": "Traer al frente",
832 | "de": "Alle nach vorn bringen",
833 | "pt-BR": "Trazer tudo para frente",
834 | "sk": "Presunúť všetko do popredia",
835 | "ru": "Все окна — на передний план",
836 | "uk": "Усі вікна наперед",
837 | "ar": "إحضار الكُل إلى المُقدِّمة",
838 | "nl": "Breng alles naar de voorgrond",
839 | "it": "Porta tutte le finestre in primo piano",
840 | "zh": "全部置于顶层",
841 | "tr": "Öne Çıkart",
842 | "hr": "Postavi sve naprijed",
843 | "hu": "Hozz mindent előre",
844 | "sv": "Ta alla fram",
845 | "ml": "എല്ലാത്തിനെയും മുന്നിലേക്ക് കൊണ്ടുവരിക",
846 | "cs": "Přesunout vše do popředí",
847 | "vn": "Mang tất cả tập tin lên trước"
848 | },
849 | "menu.window.window": {
850 | "en": "Window",
851 | "da": "Vindue",
852 | "fr": "Fenêtre",
853 | "pl": "Okno",
854 | "ja": "ウィンドウ",
855 | "es": "Ventana",
856 | "de": "Fenster",
857 | "pt-BR": "Janela",
858 | "sk": "Okno",
859 | "ru": "Окно",
860 | "uk": "Вікно",
861 | "ar": "نافِذة",
862 | "nl": "Venster",
863 | "it": "Finestra",
864 | "zh": "窗口",
865 | "tr": "Pencere",
866 | "hr": "Prozor",
867 | "hu": "Ablak",
868 | "sv": "Fönster",
869 | "ml": "വിന്ഡോ",
870 | "cs": "Okno",
871 | "vn": "Màn cửa sổ"
872 | },
873 | "menu.window.close": {
874 | "en": "Close",
875 | "da": "Luk",
876 | "fr": "Fermer",
877 | "pl": "Zamknij",
878 | "ja": "閉じる",
879 | "es": "Cerrar",
880 | "de": "Schließen",
881 | "pt-BR": "Fechar",
882 | "sk": "Zavrieť",
883 | "ru": "Закрыть",
884 | "uk": "Вийти",
885 | "ar": "إغلاق",
886 | "nl": "Sluiten",
887 | "it": "Chiudi",
888 | "zh": "关闭",
889 | "tr": "Kapat",
890 | "hr": "Zatvori",
891 | "hu": "Bezárás",
892 | "sv": "Stäng",
893 | "ml": "അടയ്ക്കുക",
894 | "cs": "Zavřít",
895 | "vn": "Đóng"
896 | },
897 |
898 | "menu.help.name": {
899 | "en": "Help",
900 | "da": "Hjælp",
901 | "fr": "Aide",
902 | "pl": "Pomoc",
903 | "ja": "ヘルプ",
904 | "es": "Ayuda",
905 | "de": "Hilfe",
906 | "pt-BR": "Ajuda",
907 | "sk": "Pomoc",
908 | "ru": "Справка",
909 | "uk": "Довідка",
910 | "ar": "المُساعدة",
911 | "nl": "Help",
912 | "it": "Aiuto",
913 | "zh": "帮助",
914 | "tr": "Yardım",
915 | "hr": "Pomoć",
916 | "hu": "Súgó",
917 | "sv": "Hjälp",
918 | "ml": "സഹായം",
919 | "cs": "Pomoc",
920 | "vn": "Hỗ trợ"
921 | },
922 | "menu.help.website": {
923 | "en": "Website",
924 | "da": "Webside",
925 | "fr": "Site Web",
926 | "pl": "Przejdź do strony internetowej",
927 | "ja": "ウェブサイト",
928 | "es": "Sitio Web",
929 | "de": "Webseite",
930 | "pt-BR": "Website",
931 | "sk": "Webová stránka",
932 | "ru": "Сайт",
933 | "uk": "Сайт",
934 | "ar": "المُساعدة",
935 | "nl": "Website",
936 | "it": "Sito web",
937 | "zh": "转到网站",
938 | "tr": "Website",
939 | "hr": "Web stranica",
940 | "hu": "Weboldal",
941 | "sv": "Hemsida",
942 | "ml": "വെബ്സൈറ്റ്",
943 | "cs": "Webová stránka",
944 | "vn": "Dịa chỉ web của chúng tôi"
945 | },
946 | "menu.help.source-code": {
947 | "en": "Source Code",
948 | "da": "Kilde Kode",
949 | "fr": "Code source",
950 | "pl": "Przejdź do źródeł programu",
951 | "ja": "ソースコード",
952 | "es": "Código fuente",
953 | "de": "Quelltext",
954 | "pt-BR": "Código-fonte",
955 | "sk": "Zdrojový kód",
956 | "ru": "Исходный код",
957 | "uk": "Програмний код",
958 | "ar": "كود المَصدر",
959 | "nl": "Broncode",
960 | "it": "Codice sorgente",
961 | "zh": "源码",
962 | "tr": "Kaynak Kod",
963 | "hr": "Izvorni kod",
964 | "hu": "Forráskód",
965 | "sv": "Källkod",
966 | "ml": "സോഴ്സ് കോഡ്",
967 | "cs": "Zdrojový kód",
968 | "vn": "Mã nguồn của Exif Cleaner"
969 | },
970 | "menu.help.report-issue": {
971 | "en": "Report an Issue",
972 | "da": "Rapportér et problem ",
973 | "fr": "Signaler un problème",
974 | "pl": "Zgłoś problem",
975 | "ja": "バグを報告",
976 | "es": "Reportar un problema",
977 | "de": "Problem melden",
978 | "pt-BR": "Informar um problema",
979 | "sk": "Nahlásiť problém",
980 | "ru": "Сообщить о проблеме",
981 | "uk": "Повідомити про проблему",
982 | "ar": "الإبلاغ عن خطأ",
983 | "nl": "Meld een Probleem",
984 | "it": "Segnala un problema",
985 | "zh": "报告错误",
986 | "tr": "Sorun bildir",
987 | "hr": "Prijavi problem",
988 | "hu": "Hiba jelentése",
989 | "sv": "Rappotera Ett Problem",
990 | "ml": "ഒരു പ്രശ്നം രേഖപ്പെടുത്തുക",
991 | "cs": "Nahlásit problém",
992 | "vn": "Báo cáo lỗi"
993 | },
994 | "menu.help.about": {
995 | "en": "About ",
996 | "da": "Om",
997 | "fr": "À propos d'",
998 | "pl": "O programie ",
999 | "ja": "概要 ",
1000 | "es": "Acerca de ",
1001 | "de": "Über ",
1002 | "pt-BR": "Sobre ",
1003 | "sk": "O programe ",
1004 | "ru": "О ",
1005 | "uk": "Про ",
1006 | "ar": "حَول",
1007 | "nl": "Over ",
1008 | "it": "Informazioni su ",
1009 | "zh": "关于",
1010 | "tr": "Hakkında",
1011 | "hr": "Informacije",
1012 | "hu": "Névjegy",
1013 | "sv": "Om",
1014 | "ml": "കുറിച്ച്",
1015 | "cs": "O programu",
1016 | "vn": "Về phần mềm"
1017 | },
1018 |
1019 | "aboutwindow:copyright": {
1020 | "en": "Copyright",
1021 | "da": "Copyright",
1022 | "fr": "Copyright",
1023 | "pl": "Copyright",
1024 | "ja": "著作",
1025 | "es": "Derechos de Autor",
1026 | "de": "Urheberrecht",
1027 | "pt-BR": "Direitos autorais",
1028 | "sk": "Autorské práva",
1029 | "ru": "Авторские права",
1030 | "uk": "Авторське право",
1031 | "ar": "حقوق النَّشر",
1032 | "nl": "Copyright",
1033 | "it": "Copyright",
1034 | "zh": "版权",
1035 | "tr": "Telif hakkı",
1036 | "hr": "Autorska prava",
1037 | "hu": "Szerzői jog",
1038 | "sv": "Copyright",
1039 | "ml": "പകർപ്പവകാശം",
1040 | "cs": "Autorská práva",
1041 | "vn": "Bản quyền"
1042 | },
1043 |
1044 | "usertasks:open-file.label": {
1045 | "en": "Open files",
1046 | "da": "Åbn filer",
1047 | "fr": "Ouvrir les fichiers",
1048 | "pl": "Otwórz pliki",
1049 | "de": "Öffne Dateien",
1050 | "pt-BR": "Abrir arquivos",
1051 | "sk": "Otvoriť súbory",
1052 | "ru": "Открыть файлы",
1053 | "uk": "Відкрити файли",
1054 | "ar": "الملفَّات المفتوحة",
1055 | "nl": "Bestanden openen",
1056 | "it": "Apri files",
1057 | "zh": "打开文件",
1058 | "tr": "Dosyaları aç",
1059 | "hr": "Otvori datoteke",
1060 | "hu": "Fájlok megnyitása",
1061 | "sv": "Öppna Filer",
1062 | "ml": "ഫയലുകൾ തുറക്കുക",
1063 | "cs": "Otevřít soubory",
1064 | "vn": "Mở tập tin"
1065 | },
1066 | "usertasks:open-file.description": {
1067 | "en": "Remove metadata from selected files",
1068 | "da": "Fjern metadata fra valgte filer",
1069 | "fr": "Supprimer les métadonnées des fichiers sélectionnés",
1070 | "pl": "Usuń metadane z wybranych plików",
1071 | "de": "Entferne Metadaten von ausgewählten Dateien",
1072 | "pt-BR": "Remover metadados dos arquivos selecionados",
1073 | "sk": "Odstrániť metadáta z vybraných súborov",
1074 | "ru": "Удалить метаданные из выбранных файлов",
1075 | "uk": "Видалити метадані з вибранних файлів",
1076 | "ar": "إزالة البيانات الوصفيَّة من الملفَّات المُحدَّدة",
1077 | "nl": "Verwijder metagegevens van de geselecteerde bestanden",
1078 | "it": "Rimuovi i metadati dai file selezionati",
1079 | "zh": "从所选文件中删除元数据",
1080 | "tr": "Seçili dosyalardan meta verileri kaldır",
1081 | "hr": "Ukloni metapodatke iz odabranih datoteka",
1082 | "hu": "Metaadatok eltávolítása a kijelölt fájlokból",
1083 | "sv": "Ta bort metadata från valda filer",
1084 | "ml": "തിരഞ്ഞെടുത്ത ഫയലുകളിൽ നിന്ന് മെറ്റാഡാറ്റ ഇല്ലാതാക്കുക",
1085 | "cs": "Odstranit metadata z vybraných souborů",
1086 | "vn": "Xóa siêu dữ liệu từ tập tin được chọn"
1087 | }
1088 | }
1089 |
--------------------------------------------------------------------------------
/.resources/win/bin/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/.resources/win/bin/.keep
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # os: osx
2 | # osx_image: xcode10.2
3 | language: node_js
4 | node_js:
5 | - "14"
6 | - "16"
7 | script:
8 | - yarn run lint
9 | - tsc
10 | # - yarn run update-exiftool
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Next release (WIP)
4 |
5 | - Add CI pipeline with Travis CI
6 |
7 | ### Features
8 |
9 | - Improved dark mode styling and start screen icon
10 | - Add translations for Croatian and Turkish
11 |
12 | ### Infrastructure
13 |
14 | - Remove node-sass and sass-loader dev dependencies
15 | - Remove Spectre Sass framework dependency and replace it with plain CSS using CSS variables
16 |
17 | ## 3.6.0 - 4 May 2021
18 |
19 | ### Security
20 |
21 | - Fix for XSS and Electron reverse shell vulnerabilities by sanitizing `exiftool` HTML output in the UI. To take advantage of this, an attacker would have had to write image metadata containing malicious script code to a file that you then download and run through ExifCleaner. Proofs of concept:
22 |
23 | XSS:
24 |
25 | ```bash
26 | exiftool -Comment='
OverJT' -PixelUnits='meters' image.png
27 | ```
28 |
29 | Electron reverse shell:
30 |
31 | ```bash
32 | exiftool -Comment='
OverJT' -PixelUnits='meters' image.png
33 | ```
34 |
35 | ## 3.5.1 - 1 May 2021
36 |
37 | ## Infrastructure
38 |
39 | - Add support for Windows "portable" releases that don't require installation
40 |
41 | ## 3.5.0 - 1 May 2021
42 |
43 | If you are running a previous version of ExifCleaner, update immediately due to a security vulnerability found in exiftool (the command-line tool that ExifCleaner uses under the hood). Thank you to all contributors for this release. As always, credits are listed in the README.
44 |
45 | ### Security
46 |
47 | - Update exiftool to 12.25 to mitigate [CVE-2021-22204 arbitrary code execution](https://twitter.com/wcbowling/status/1385803927321415687)
48 |
49 | ### Features
50 |
51 | - Add translations for Slovak, Russian, Ukranian, Danish, Arabic, Italian, Chinese (Mandarin)
52 | - Add support for the new Mac M1 ARM processors
53 |
54 | ### Infrastructure
55 |
56 | - Upgrade to Electron 11
57 | - Update some NPM dependencies
58 | - Start maintaining a CHANGELOG file in source control
59 |
60 | ### Fixes
61 |
62 | - Translation fixes for Portuguese (Brazil) and French
63 | - Update Linux AppImage category to fix exit status 1 issue
64 |
65 | ## 3.4.0 - 19 Oct 2020
66 |
67 | ### Features
68 |
69 | - Huge speed increase for file processing, especially when batch processing many files with multiple CPUs (more efficient process pool algorithm, better integration with exiftool process keep-alive)
70 | - Multilingual support with translations for French, Polish, Japanese, Spanish (Spain), German, and Portuguese (Brazil)
71 | - Mac/Windows: show progress in dock when batch processing files
72 | - Linux: fix app icon in dock
73 | - Linux: dark mode works with Ubuntu
74 |
75 | ### Bug Fixes
76 |
77 | - Linux: fix issue where icon.png was not found on startup with .deb installs
78 |
79 | ### Infrastructure
80 |
81 | - Upgrade to Electron 10
82 | - Upgrade to exiftool 12.08
83 | - Add update_exiftool.pl Perl script to automate pulling down latest ExifTool binaries and verifying their checksums
84 | - Remove a bunch of NPM dependencies
85 |
86 | ## 3.3.1 - 11 Jul 2020
87 |
88 | - Change from JavaScript to TypeScript for improved stability of compiler static analysis.
89 | - Fix Windows UTF-8 filename bug.
90 | - Remove several NPM dependencies to simplify code.
91 | - Upgrade to Electron 9.
92 | - Minor UI polish.
93 |
94 | ## 3.2.0 - 27 Apr 2020
95 |
96 | - Fix Linux version (was not using correct ExifTool binary path)
97 | - Add File -> Open menu item
98 | - Add dock icon for Linux AppImage
99 | - Mac quit entire app when File -> Close menu item is selected
100 | - Linux clean up About screen
101 | - Update app start text to show that ExifCleaner also supports video and PDF files.
102 |
103 | ## 3.1.0 - 3 Feb 2020
104 |
105 | - Drop target should follow window size when you resize it to be taller
106 | - Set a minimium window size in BrowserWindow
107 | - On macOS, when you close the window, the app should quit.
108 | - night mode better icon display opacity
109 | - night mode font not so thin
110 | - remove Automatic updates from README (feature removed)
111 |
112 | ## 3.0.0 - 18 Jan 2020
113 |
114 | - properly clean up after exiftool perl5.18 processes
115 | - disable auto update
116 | - remove esm dep. fix dev env
117 | - disable unused preferences menu item. esm modules for import with node
118 |
119 | ## 2.1.0 - 10 Jan 2020
120 |
121 | - electron 7.1.8 which should fix auto update issue in electron-build, according to some developer reports
122 |
123 | ## 2.0.0 - 4 Jan 2020
124 |
125 | - electron 7.1.2 to fix electron-builder auto update regression
126 |
127 | ## 1.5.1 - 10 Dec 2019
128 |
129 | - fix node url require
130 |
131 | ## 1.5.0 - 10 Dec 2019
132 |
133 | - drastically simplify dark mode code
134 | - debugging dark mode in Electron 6. clean up js functions/modules
135 |
136 | ## 1.4.0 - 10 Dec 2019
137 |
138 | - downgrade to Electron 6 to fix auto-update
139 |
140 | ## 1.3.5 - 10 Dec 2019
141 |
142 | - fix mainwindow callback null error
143 |
144 | ## 1.3.4 - 10 Dec 2019
145 |
146 | - Automatic updates logger fix
147 |
148 | ## 1.3.3 - 10 Dec 2019
149 |
150 | - Auto-updater debug logging
151 |
152 | ## 1.3.1 - 10 Dec 2019
153 |
154 | - Fix popover hover bounds
155 |
156 | ## 1.3.0 - 8 Dec 2019
157 |
158 | - Fix popover transparency
159 | - Fix dark mode font color for exif values
160 |
161 | ## 1.1.0 - 8 Dec 2019
162 |
163 | - First release.
164 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) szTheory (https://exifcleaner.app)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
ExifCleaner
2 |
3 |  
4 |
5 | > Desktop app to clean metadata from images, videos, PDFs, and other files.
6 |
7 | 
8 |
9 | ## !!!!! NOTE - UPGRADE TO 3.6.0+ ASAP !!!!!
10 |
11 | If you are running a version of ExifCleaner before 3.6.0, upgrade immediately! A security vulnerability was found in exiftool, the command-line application that powers ExifCleaner under the hood, and this was updated in ExifCleaner 3.5.0. There was also an XSS and Electron remote shell vulnerability due to unsanitized HTML output that was fixed in ExifCleaner 3.6.0.
12 |
13 | ## Benefits
14 |
15 | - Fast
16 | - Drag & Drop
17 | - Free and open source (MIT)
18 | - Windows, Mac, and Linux
19 | - Supports popular image formats such as PNG, JPG, GIF, and TIFF
20 | - Supports popular video formats such as M4A, MOV, and MP4
21 | - Supports PDF documents\* (partial, [see discussion](https://github.com/szTheory/exifcleaner/issues/111))
22 | - Batch-processing
23 | - Multi-core support
24 | - Dark mode (automatic)
25 | - No automatic updates or network traffic
26 | - Multi-language support
27 | - Relatively few NPM dependencies (no JS frameworks)
28 |
29 | ## Drawbacks
30 |
31 | - Executable size `~200MB` (Electron app)
32 | - Memory usage `~120MB` (Electron app)
33 | - PDF metadata removal is only partial ([see discussion](https://github.com/szTheory/exifcleaner/issues/111))
34 | - Does not remove extended filesystem attributes ([see discussion](https://github.com/szTheory/exifcleaner/issues/86))
35 |
36 | ## Download and Install
37 |
38 | Linux, macOS 10.10+, and Windows 7+ are supported (64-bit only).
39 |
40 | - **Linux**: [Download the .AppImage, .deb, or .rpm file](https://github.com/szTheory/exifcleaner/releases/latest)
41 | - **macOS**: [Download the .dmg file](https://github.com/szTheory/exifcleaner/releases/latest)
42 | - **Windows**: [Download the .exe file](https://github.com/szTheory/exifcleaner/releases/latest)
43 |
44 | For Linux, The AppImage needs to be [made executable](https://discourse.appimage.org/t/how-to-make-an-appimage-executable/80) after download.
45 |
46 | Arch Linux users can install the app from the AUR using an AUR helper (such as `yay` or `paru`):
47 |
48 | ```bash
49 | paru -S exifcleaner-bin
50 | ```
51 |
52 | ## Links
53 |
54 | - [Official Website](https://exifcleaner.com)
55 | - [Download](https://github.com/szTheory/exifcleaner/releases)
56 | - [Source Code](https://github.com/szTheory/exifcleaner)
57 | - [Issue Tracker](https://github.com/szTheory/exifcleaner/issues)
58 | - [Translations file](https://github.com/szTheory/exifcleaner/blob/master/.resources/strings.json)
59 |
60 | ## Supported File Types
61 |
62 | Below is a full list of supported file types that ExifCleaner will remove metadata for. It's based on which file types [ExifTool](https://exiftool.org/) supports write operations for.
63 |
64 | - **3G2, 3GP2** – 3rd Gen. Partnership Project 2 a/v (QuickTime-based)
65 | - **3GP, 3GPP** – 3rd Gen. Partnership Project a/v (QuickTime-based)
66 | - **AAX** – Audible Enhanced Audiobook (QuickTime-based)
67 | - **AI, AIT** – Adobe Illustrator [Template] (PS or PDF)
68 | - **ARQ** – Sony Alpha Pixel-Shift RAW (TIFF-based)
69 | - **ARW** – Sony Alpha RAW (TIFF-based)
70 | - **AVIF** – AV1 Image File Format (QuickTime-based)
71 | - **CR2** – Canon RAW 2 (TIFF-based) (CR2 spec)
72 | - **CR3** – Canon RAW 3 (QuickTime-based) (CR3 spec)
73 | - **CRM** – Canon RAW Movie (QuickTime-based)
74 | - **CRW, CIFF** – Canon RAW Camera Image File Format (CRW spec)
75 | - **CS1** – Sinar CaptureShop 1-shot RAW (PSD-based)
76 | - **DCP DNG** – Camera Profile (DNG-like)
77 | - **DNG** – Digital Negative (TIFF-based)
78 | - **DR4** – Canon DPP version 4 Recipe
79 | - **DVB** – Digital Video Broadcasting (QuickTime-based)
80 | - **EPS, EPSF, PS** – [Encapsulated] PostScript Format
81 | - **ERF** – Epson RAW Format (TIFF-based)
82 | - **EXIF** – Exchangeable Image File Format metadata (TIFF-based)
83 | - **EXV** – Exiv2 metadata file (JPEG-based)
84 | - **F4A, F4B, F4P, F4V** – Adobe Flash Player 9+ Audio/Video (QuickTime-based)
85 | - **FFF** – Hasselblad Flexible File Format (TIFF-based)
86 | - **FLIF** – Free Lossless Image Format
87 | - **GIF** – Compuserve Graphics Interchange Format
88 | - **GPR** – GoPro RAW (DNG-based)
89 | - **HDP, WDP, JXR** – Windows HD Photo / Media Photo / JPEG XR (TIFF-based)
90 | - **HEIC, HEIF** – High Efficiency Image Format (QuickTime-based)
91 | - **ICC, ICM** – International Color Consortium color profile
92 | - **IIQ** – Phase One Intelligent Image Quality RAW (TIFF-based)
93 | - **IND, INDD, INDT** – Adobe InDesign Document/Template
94 | - **INSP** – Insta360 Picture (JPEG-based)
95 | - **JP2, JPF, JPM, JPX** – JPEG 2000 image [Compound/Extended]
96 | - **JPEG, JPG, JPE** – Joint Photographic Experts Group image
97 | - **LRV** – Low-Resolution Video (QuickTime-based)
98 | - **M4A, M4B, M4P, M4V** – MPEG-4 Audio/Video (QuickTime-based)
99 | - **MEF** – Mamiya (RAW) Electronic Format (TIFF-based)
100 | - **MIE** – Meta Information Encapsulation (MIE specification)
101 | - **MOS** – Creo Leaf Mosaic (TIFF-based)
102 | - **MOV, QT** – Apple QuickTime Movie
103 | - **MP4** – Motion Picture Experts Group version 4 (QuickTime-based)
104 | - **MPO** – Extended Multi-Picture format (JPEG with MPF extensions)
105 | - **MQV** – Sony Mobile QuickTime Video
106 | - **NEF** – Nikon (RAW) Electronic Format (TIFF-based)
107 | - **NRW** – Nikon RAW (2) (TIFF-based)
108 | - **ORF** – Olympus RAW Format (TIFF-based)
109 | - **PDF** – Adobe Portable Document Format
110 | - **PEF** – Pentax (RAW) Electronic Format (TIFF-based)
111 | - **PNG, JNG, MNG** – Portable/JPEG/Multiple-image Network Graphics
112 | - **PPM, PBM, PGM** – Portable Pixel/Bit/Gray Map
113 | - **PSD, PSB, PSDT** – PhotoShop Document / Large Document / Template
114 | - **QTIF, QTI, QIF** – QuickTime Image File
115 | - **RAF** – FujiFilm RAW Format
116 | - **RAW** – Panasonic RAW (TIFF-based)
117 | - **RW2** – Panasonic RAW 2 (TIFF-based)
118 | - **RWL** – Leica RAW (TIFF-based)
119 | - **SR2** – Sony RAW 2 (TIFF-based)
120 | - **SRW** – Samsung RAW format (TIFF-based)
121 | - **THM** – Thumbnail image (JPEG)
122 | - **TIFF, TIF** – Tagged Image File Format
123 | - **VRD** – Canon DPP Recipe Data
124 | - **X3F** – Sigma/Foveon RAW
125 | - **XMP** – Extensible Metadata Platform sidecar file
126 |
127 | ## File writer limitations
128 |
129 | ExifCleaner has the same writer limitations as the underlying `exiftool` it depends on. Taken from the [official website](https://exiftool.org/#limitations):
130 |
131 | - ExifTool will not rewrite a file if it detects a significant problem with the file format.
132 | - ExifTool has been tested with a wide range of different images, but since it is not possible to test it with every known image type, there is the possibility that it will corrupt some files. Be sure to keep backups of your files.
133 | - Even though ExifTool does some validation of the information written, it is still possible to write illegal values which may cause problems when reading the images with other software. So take care to validate the information you are writing.
134 | - ExifTool is not guaranteed to remove metadata completely from a file when attempting to delete all metadata. For JPEG images, all APP segments (except Adobe APP14, which is not removed by default) and trailers are removed which effectively removes all metadata, but for other formats the results are less complete:
135 | - JPEG - APP segments (except Adobe APP14) and trailers are removed.
136 | - TIFF - XMP, IPTC, ICC_Profile and the ExifIFD are removed, but some EXIF may remain in IFD0. (The CommonIFD0 Shortcut tag is provided to simplify removal of common metadata tags from IFD0.)
137 | - PNG - Only XMP, EXIF, ICC_Profile and native PNG textual data chunks are removed.
138 | - PDF - The original metadata is never actually removed.
139 | - PS - Only XMP and some native PostScript tags may be deleted.
140 | - MOV/MP4 - Most top-level metadata is removed.
141 | - RAW formats - It is not recommended to remove all metadata from RAW images because this will likely remove some proprietary information that is necessary for proper rendering of the image.
142 |
143 | ## Translations
144 |
145 | New translations and corrections to existing translations are welcome! See the [Adding a Translation](https://github.com/szTheory/exifcleaner/#adding-a-translation) section if there is a language you would like to add. Here is the current translations status:
146 |
147 | - Arabic ✅ by [@ZER0-X](https://github.com/ZER0-X)
148 | - Chinese (Mandarin) ✅ by [MarcusPierce](https://github.com/MarcusPierce)
149 | - Croatian ✅ by [@milotype](https://github.com/milotype)
150 | - Czech ✅ by [@t0mzSK](https://github.com/t0mzSK)
151 | - Danish ✅ by [@zlatco](https://github.com/zlatco)
152 | - Dutch ✅ by [@rvl-code](https://github.com/rvl-code)
153 | - French (France) ✅ by [@NathanBnm](https://github.com/NathanBnm) (Nathan Bonnemains)
154 | - French (Quebec) ❌ needs translation if France version is not sufficient
155 | - German ✅ by [@tayfuuun](https://github.com/tayfuuun), with updates by [@philippsandhaus](https://github.com/philippsandhaus)
156 | - Hungarian ✅ by [@icetee](https://github.com/icetee) (Tamás András Horváth)
157 | - Italian ✅ by [@PolpOnline](https://github.com/PolpOnline)
158 | - Japanese ✅ by @AKKED
159 | - Malayalam by ✅ by [@theunknownKiran](https://github.com/theunknownKiran)
160 | - Polish ✅ by [@m1chu](https://github.com/m1chu)
161 | - Portuguese (Brazil) ✅ by [@iraamaro](https://github.com/iraamaro), with updates by @dadodollabela
162 | - Portuguese (Portugal) ❌ needs translation if Brazil version is not sufficient
163 | - Russian ✅ by [@likhner](https://github.com/likhner) (Arthur Likhner)
164 | - Spanish (Spain) ✅ by [@ff-ss](https://github.com/ff-ss) (Francisco)
165 | - Spanish (Latin America) ❌ needs translation if Spain version is not sufficient
166 | - Swedish ✅ by [@sastofficial](https://github.com/sastofficial)
167 | - Slovak ✅ by [@LiJu09](https://github.com/LiJu09)
168 | - Turkish ✅ by [@bsonmez](https://github.com/bsonmez) (Burak Sonmez)
169 | - Ukranian ✅ by [@hugonote](https://github.com/hugonote) (Alexander Berger)
170 | - Vietnamese ✅ by [@tensingnightco](https://github.com/tensingnightco)
171 |
172 | ## Verifying checksum of downloads from the Github releases page
173 |
174 | Download the `latest.yml` (Windows), `latest-mac.yml` (Mac), or `latest-linux.yml` (Linux) file from the release page that corresponds to your operating system. Then run the following command to generate a sha checksum. ExifCleaner 3.5.0 is used here as an example.
175 |
176 | On Mac, Linux, and on Windows using the Linux Subsystem for Windows:
177 |
178 | ```bash
179 | sha512sum ExifCleaner-Setup-3.5.0.exe | cut -f1 -d\ | xxd -r -p | base64
180 | ```
181 |
182 | The output should match the sha512 value in the latest.yml file for the version you downloaded. As of now there is no checksum generated for the Linux RPM version (appears to be an electron-build issue, see [Github issue here](https://github.com/szTheory/exifcleaner/issues/141)).
183 |
184 | ## Development
185 |
186 | Built with [Electron](https://electronjs.org). Uses [node-exiftool](https://www.npmjs.com/package/node-exiftool) as a wrapper for [Exiftool](https://exiftool.org/) binaries. To see the current list of NPM dependencies, run:
187 |
188 | ```bash
189 | yarn list --production
190 | ```
191 |
192 | ### Run the app in dev mode
193 |
194 | Clone the repository and cd into the directory.
195 |
196 | ```bash
197 | git clone https://github.com/szTheory/exifcleaner.git
198 | cd exifcleaner
199 | ```
200 |
201 | Next, install the NPM package dependencies.
202 |
203 | ```bash
204 | yarn install
205 | ```
206 |
207 | Pull down the latest ExifTool binaries (in Windows, run this within the Linux Subsystem for Windows):
208 |
209 | ```bash
210 | yarn run update-exiftool
211 | ```
212 |
213 | Finally, launch the application. This supports Hot Module Reload (HMR) so you will automatically see your changes every time you save a file.
214 |
215 | ```bash
216 | yarn run dev
217 | ```
218 |
219 | ### Contributing
220 |
221 | This app is mostly feature complete. I want to keep it simple and not add a bunch of bloat to it. And I want to avoid release churn. That said, there are a couple small features that might be worth adding. And there are a few minor bugs or points of cleanup that would be worth polishing. If you'd like to help check out the [Issue Tracker](https://github.com/szTheory/exifcleaner/issues) which contains an exhaustive list of known issues. Just pick one and submit a Pull Request or leave a comment and I can provide guidance or help if you need it. Make sure to test the app out to see if it still works though. There isn't much going on in this app so it should be easy enough to do. I might add some automated tests later on to help with this. For now it's just been me working on the app so manual testing has worked out fine.
222 |
223 | TypeScript code is formatted using Prettier.
224 |
225 | ### Adding a Translation
226 |
227 | Adding a translation is easy. All you have to do is go to [the translation list](https://github.com/szTheory/exifcleaner/blob/master/.resources/strings.json), click on "Edit this file", and add an entry for the new language underneath the other ones. So for example if you wanted to add a Spanish translation, where it says:
228 |
229 | ```json
230 | "empty.title": {
231 | "en": "No files selected",
232 | "fr": "Aucun fichier sélectionné"
233 | },
234 | ```
235 |
236 | You just add a line for `"es"` (list of language codes [here](https://www.electronjs.org/docs/api/locales)) underneath the other ones:
237 |
238 | ```json
239 | "empty.title": {
240 | "en": "No files selected",
241 | "fr": "Aucun fichier sélectionné",
242 | "es": "Spanish translation here"
243 | },
244 | ```
245 |
246 | and repeat that pattern for each of the entries. That's probably the easiest way to contribute. If you want to be able to see all of your translations working in a live app before submitting, you can also do this:
247 |
248 | 1. Fork the project on Github
249 | 2. Follow the directions [here](https://github.com/szTheory/exifcleaner#run-the-app-in-dev-mode) to get ExifCleaner running in development mode on your computer
250 | 3. Then update the `strings.json` file as mentioned above, and quit the program and relaunch it to see your changes. When you're finished, commit your changes from the command line with for example `git commit -am "Finished adding translations"`. Then run `git push origin master`, and go to the project URL your forked it to (for example ) and click the button to open a new Pull Request.
251 |
252 | If you want to run the app with a specific locale without changing your system preferences, use one of the following commands with the correct language code. If you don't see your language listed below, just follow the pattern and plug in your own language code [from this list](https://www.electronjs.org/docs/api/locales).
253 |
254 | ```bash
255 | yarn run dev --lang=en #English
256 | yarn run dev --lang=fr #French
257 | yarn run dev --lang=pl #Polish
258 | yarn run dev --lang=ja #Japanese
259 | yarn run dev --lang=es #Spanish
260 | yarn run dev --lang=de #German
261 | ```
262 |
263 | Let me know if you run into any issues, I can guide you through the process if you get stuck.
264 |
265 | ### Linux AppImage Notes
266 |
267 | To mount the AppImage and inspect it's contents:
268 |
269 | ```bash
270 | ./ExifCleaner-x.y.z.AppImage --appimage-mount
271 | ```
272 |
273 | Where `x.y.z` is the release version number
274 |
275 | ### Smoke test checklist for new releases
276 |
277 | On all platforms:
278 |
279 | - Linux
280 | - Windows
281 | - Mac
282 |
283 | Perform the following manual tests before a release:
284 |
285 | - Drag and drop hundreds of files
286 | - File -> Open dialog
287 | - Switch locale to each language and check translations
288 | - Switch between light and dark mode
289 | - Open "About" dialog
290 |
291 | ### Publishing a new release
292 |
293 | This section is really for my own reference when publishing a new release.
294 |
295 | Bump the version with `release` (choose a "pre" release for point releases for testing):
296 |
297 | ```bash
298 | yarn run release
299 | ```
300 |
301 | Check the [Github release page](https://github.com/szTheory/exifcleaner/releases) and confirm a new draft release was created. Then run the publish command:
302 |
303 | ```bash
304 | yarn run publish
305 | ```
306 |
307 | Once you're happy with the release and want to finalize it, remove the draft flag on the Github releases page.
308 |
309 | ### Contributors
310 |
311 | Thanks to all the people who submitted bug reports and fixes. I've tried to include everyone so if I've missed you it was by accident, just let me know and I'll add you.
312 |
313 | - [@m1chu](https://github.com/m1chu) - Polish translation, fix for Mac dock bug on non-Mac platforms, help debugging Unicode filename bug
314 | - [@LukasThyWalls](https://github.com/LukasThyWalls) - help debugging Unicode filename bug, feature suggestions
315 | - @AKKED - Japanese translation, help debugging Unicode filename bug
316 | - [@TomasGutierrez0](https://github.com/TomasGutierrez0) - help auditing ExifTool dependency
317 | - [@5a384507-18ce-417c-bb55-d4dfcc8883fe](https://github.com/5a384507-18ce-417c-bb55-d4dfcc8883fe) - help debugging initial Linux version
318 | - [@totoroot](https://github.com/totoroot) - help debugging Linux AppImage installer, usability feedback, feature suggestions
319 | - [@Scopuli](https://github.com/Scopuli) - help debugging Linux AppImage installer
320 | - [@Tox86](https://github.com/Tox86) - found broken Settings menu item bug
321 | - [@ff-ss](https://github.com/ff-ss) (Francisco) - Spanish translation
322 | - [@tayfuuun](https://github.com/tayfuuun) - German translation
323 | - [@philippsandhaus](https://github.com/philippsandhaus) - German translation fixes
324 | - [@airvue](https://github.com/airvue) - Help debugging Ubuntu .deb package error
325 | - [@Goblin80](https://github.com/Goblin80) - Help debugging Ubuntu .deb package error
326 | - [@zahroc](https://github.com/zahroc) - Help diagnosing error when adding bulk directories
327 | - [@iraamaro](https://github.com/iraamaro) - Portuguese (Brazil) translation. Fix for update_exiftool.pl when building from source on Debian and Slackware
328 | - [@LiJu09](https://github.com/LiJu09) - Slovak translation
329 | - [@likhner](https://github.com/likhner) (Arthur Likhner) - Russian translation
330 | - [@hugonote](https://github.com/hugonote) (Alexander Berger) - Ukranian translation
331 | - @dadodollabela - Portuguese (Brazil) translation fixes
332 | - [@zlatco](https://github.com/zlatco) - Danish translation
333 | - [@ZER0-X](https://github.com/ZER0-X) - Arabic translation
334 | - [@rvl-code](https://github.com/rvl-code) - Dutch translation
335 | - [@PolpOnline](https://github.com/PolpOnline) - Italian translation, Arch Linux distribution maintainer
336 | - [@NathanBnm](https://github.com/NathanBnm) (Nathan Bonnemains) - French translation
337 | - [@Dyrimon](https://github.com/Dyrimon) - Linux AppImage error notification fix
338 | - [@MarcusPierce](https://github.com/MarcusPierce) - Chinese (Mandarin) translation
339 | - [@brandonlou](https://github.com/brandonlou) - Heads up on updating exiftool to 12.24+ to mitigate [CVE-2021-22204 arbitrary code execution](https://twitter.com/wcbowling/status/1385803927321415687)
340 | - [@v4k0nd](https://github.com/v4k0nd) (Szabó Krisztián) - Help building instructions on verifying release checksums
341 | - [@papb](https://github.com/papb) - Help setting up Windows portable build
342 | - [@Bellisario](https://github.com/Bellisario) - Help setting up Windows portable build
343 | - [@overjt](https://github.com/overjt) (Jonathan Toledo) - Proof of concept for XSS and Electron remote shell vulnerability
344 | - [@bsonmez](https://github.com/bsonmez) (Burak Sonmez) - Turkish translation
345 | - [@milotype](https://github.com/milotype) - Croatian translation
346 | - [@icetee](https://github.com/icetee) - Hungarian translation
347 | - [@sastofficial](https://github.com/sastofficial) - Swedish translation
348 | - [@theunknownKiran](https://github.com/theunknownKiran) - Malayalam translation
349 | - [@t0mzSK](https://github.com/t0mzSK) - Czech translation
350 | - [@tensingnightco](https://github.com/tensingnightco) - Vietnamese translation
351 |
--------------------------------------------------------------------------------
/build/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/build/background.png
--------------------------------------------------------------------------------
/build/background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/build/background@2x.png
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/szTheory/exifcleaner/ae09e1a2eb19f12083fa83f308ebeb5fecea2685/build/icon.png
--------------------------------------------------------------------------------
/electron-webpack.json:
--------------------------------------------------------------------------------
1 | {
2 | "renderer": {
3 | "template": "src/renderer/index.html"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "exifcleaner",
3 | "productName": "ExifCleaner",
4 | "version": "3.6.0",
5 | "description": "Clean exif metadata from images, videos, and PDF documents",
6 | "license": "MIT",
7 | "repository": "github:szTheory/exifcleaner",
8 | "main": "src/main/index.js",
9 | "author": {
10 | "name": "szTheory",
11 | "email": "szTheory@users.noreply.github.com",
12 | "url": "https://exifcleaner.com"
13 | },
14 | "scripts": {
15 | "update-exiftool": "./update_exiftool.pl",
16 | "postinstall": "electron-builder install-app-deps",
17 | "format": "yarn prettier --write 'src/**/*.ts'",
18 | "lint": "prettier --check 'src/**/*.ts'",
19 | "start": "electron .",
20 | "packmactest": "yarn run compile && electron-builder --dir -c.compression=store -c.mac.identity=null",
21 | "packwin": "yarn run compile && electron-builder --windows",
22 | "packlinux": "yarn run compile && electron-builder --linux",
23 | "packmac": "yarn run compile && electron-builder --macos -c.mac.identity=null",
24 | "build": "yarn run compile && electron-builder --macos --linux --windows",
25 | "publish": "yarn run compile && electron-builder --macos --linux --windows -p always",
26 | "release": "np",
27 | "dev": "electron-webpack dev",
28 | "compile": "electron-webpack"
29 | },
30 | "dependencies": {
31 | "node-exiftool": "2.3.0",
32 | "source-map-support": "^0.5"
33 | },
34 | "devDependencies": {
35 | "@types/node": "^12.0",
36 | "electron": "^11.0",
37 | "electron-builder": "^22.8",
38 | "electron-webpack": "^2.8",
39 | "electron-webpack-ts": "^4.0",
40 | "prettier": "2.1",
41 | "typescript": "^3.8",
42 | "webpack": "^4.41"
43 | },
44 | "np": {
45 | "publish": false,
46 | "releaseDraft": false
47 | },
48 | "build": {
49 | "publish": {
50 | "provider": "github",
51 | "owner": "szTheory",
52 | "repo": "exifcleaner",
53 | "protocol": "https"
54 | },
55 | "appId": "com.exifcleaner",
56 | "mac": {
57 | "category": "public.app-category.graphics-and-images",
58 | "darkModeSupport": true,
59 | "extraResources": [
60 | {
61 | "from": ".resources/nix/bin",
62 | "to": "nix/bin",
63 | "filter": [
64 | "**/*"
65 | ]
66 | }
67 | ]
68 | },
69 | "dmg": {
70 | "iconSize": 160,
71 | "contents": [
72 | {
73 | "x": 180,
74 | "y": 170
75 | },
76 | {
77 | "x": 480,
78 | "y": 170,
79 | "type": "link",
80 | "path": "/Applications"
81 | }
82 | ]
83 | },
84 | "linux": {
85 | "target": [
86 | "appImage",
87 | "deb",
88 | "rpm"
89 | ],
90 | "icon": "build/icon.icns",
91 | "category": "Graphics",
92 | "extraResources": [
93 | {
94 | "from": ".resources/nix/bin",
95 | "to": "nix/bin",
96 | "filter": [
97 | "**/*"
98 | ]
99 | }
100 | ]
101 | },
102 | "win": {
103 | "target": [
104 | {
105 | "target": "nsis",
106 | "arch": [
107 | "x64",
108 | "ia32"
109 | ]
110 | },
111 | {
112 | "target": "portable"
113 | }
114 | ],
115 | "extraResources": [
116 | {
117 | "from": ".resources/win/bin",
118 | "to": "win/bin",
119 | "filter": [
120 | "**/*"
121 | ]
122 | }
123 | ]
124 | },
125 | "extraResources": [
126 | {
127 | "from": ".resources/",
128 | "to": "",
129 | "filter": [
130 | "strings.json",
131 | "*.png"
132 | ]
133 | }
134 | ]
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/common/app.ts:
--------------------------------------------------------------------------------
1 | import { app, remote } from "electron";
2 |
3 | export function currentApp(): Electron.App {
4 | return app ?? remote.app;
5 | }
6 |
7 | export function currentAppPath(): string {
8 | return currentApp().getAppPath();
9 | }
10 |
--------------------------------------------------------------------------------
/src/common/binaries.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { getPlatform, Platform } from "./platform";
3 | import { resourcesPath } from "./resources";
4 |
5 | enum BinaryPlatformSubpath {
6 | Win = "win",
7 | Nix = "nix",
8 | }
9 |
10 | enum BinFilename {
11 | Win = "exiftool.exe",
12 | Nix = "exiftool",
13 | }
14 |
15 | function binariesPath(): string {
16 | return path.join(resourcesPath(), binaryPlatformSubpath(), "bin");
17 | }
18 |
19 | function binaryPlatformSubpath(): BinaryPlatformSubpath {
20 | const platform = getPlatform();
21 |
22 | switch (getPlatform()) {
23 | case Platform.WIN:
24 | return BinaryPlatformSubpath.Win;
25 | case Platform.NIX:
26 | case Platform.MAC:
27 | return BinaryPlatformSubpath.Nix;
28 | default:
29 | throw `Could not determine dev Exiftool binary subpath for platform ${platform}`;
30 | }
31 | }
32 |
33 | function binaryFilename(): string {
34 | const platform = getPlatform();
35 |
36 | switch (platform) {
37 | case Platform.WIN:
38 | return BinFilename.Win;
39 | case Platform.NIX:
40 | case Platform.MAC:
41 | return BinFilename.Nix;
42 | default:
43 | throw `Could not determine the ExifTool binary path for platform ${platform}`;
44 | }
45 | }
46 |
47 | function getExifToolBinPath(): string {
48 | return path.resolve(binariesPath(), binaryFilename());
49 | }
50 |
51 | export const exiftoolBinPath = getExifToolBinPath();
52 |
--------------------------------------------------------------------------------
/src/common/browser_window.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from "electron";
2 |
3 | export function currentBrowserWindow(
4 | browserWindow: BrowserWindow | null | undefined
5 | ): BrowserWindow | null {
6 | if (!browserWindow) {
7 | browserWindow = BrowserWindow.getAllWindows()[0];
8 | }
9 |
10 | return browserWindow;
11 | }
12 |
13 | export function defaultBrowserWindow(
14 | browserWindow: BrowserWindow | null | undefined
15 | ): BrowserWindow {
16 | if (!browserWindow) {
17 | browserWindow = currentBrowserWindow(browserWindow);
18 | if (!browserWindow) {
19 | throw new Error(
20 | "Could not load file open menu because browser window was not initialized."
21 | );
22 | }
23 | }
24 |
25 | return browserWindow;
26 | }
27 |
28 | export function restoreWindowAndFocus(
29 | browserWindow: BrowserWindow | null | undefined
30 | ): void {
31 | browserWindow = defaultBrowserWindow(browserWindow);
32 | if (browserWindow.isMinimized()) {
33 | browserWindow.restore();
34 | }
35 | browserWindow.show();
36 | browserWindow.focus();
37 | }
38 |
--------------------------------------------------------------------------------
/src/common/env.ts:
--------------------------------------------------------------------------------
1 | import electron from "electron";
2 |
3 | if (typeof electron === "string") {
4 | throw new TypeError("Not running in an Electron environment!");
5 | }
6 |
7 | export function isProd(): boolean {
8 | return process.env.NODE_ENV === "production";
9 | }
10 |
11 | export function isDev(): boolean {
12 | return !isProd();
13 | }
14 |
--------------------------------------------------------------------------------
/src/common/exif_tool_processes.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 | import { ExiftoolProcess } from "node-exiftool";
3 | import { exiftoolBinPath } from "../common/binaries";
4 |
5 | export function spawnExifToolProcesses(
6 | maxNumProcesses: number
7 | ): ExiftoolProcess[] {
8 | const numProcesses = Math.min(os.cpus().length, maxNumProcesses);
9 |
10 | return [...Array(numProcesses)].map((n) => {
11 | return newExifToolProcess();
12 | });
13 | }
14 |
15 | function newExifToolProcess(): ExiftoolProcess {
16 | return new ExiftoolProcess(exiftoolBinPath);
17 | }
18 |
--------------------------------------------------------------------------------
/src/common/i18n.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import { resourcesPath } from "./resources";
4 |
5 | // Locales list: https://www.electronjs.org/docs/api/locales
6 | export enum Locale {
7 | Chinese = "zh",
8 | English = "en",
9 | French = "fr",
10 | German = "de",
11 | Italian = "it",
12 | Japanese = "ja",
13 | Polish = "pl",
14 | Portuguese = "pt",
15 | Russian = "ru",
16 | Spanish = "es",
17 | Hungarian = "hu",
18 | }
19 |
20 | type I18nStringSet = {
21 | [locale: string]: string;
22 | };
23 |
24 | type I18nStringsDictionary = {
25 | [key: string]: I18nStringSet;
26 | };
27 |
28 | let strings: I18nStringsDictionary | null = null;
29 |
30 | export function i18n(key: string, locale: string): string {
31 | if (!strings) {
32 | throw new Error("i18n strings file not loaded");
33 | }
34 |
35 | const i18nString = strings[key];
36 | if (!i18nString) {
37 | throw new Error(
38 | `Could not find localization strings while reading text for ${key}`
39 | );
40 | }
41 | // prefer locale, then fallback locale, then default to English
42 | const text =
43 | i18nString[locale] ||
44 | i18nString[fallbackLocale(locale)] ||
45 | i18nString[Locale.English];
46 | if (!text) {
47 | throw new Error(`Could not find interface text for ${key}`);
48 | }
49 |
50 | return text;
51 | }
52 |
53 | export function preloadI18nStrings(): void {
54 | if (strings) {
55 | return;
56 | }
57 |
58 | strings = JSON.parse(stringsFile());
59 | }
60 |
61 | // Select a fallback for each "dialect" if it doesn't already
62 | // have its own translation more specific than the main entry
63 | // Locales list: https://www.electronjs.org/docs/api/locales
64 | export function fallbackLocale(locale: string): string {
65 | switch (locale) {
66 | case "zh-CN": //Chinese (Simplified)
67 | case "zh-TW": //Chinese (Traditional)
68 | return Locale.Chinese;
69 |
70 | case "fr-CA": //French (Canada)
71 | case "fr-CH": //French (Switzerland)
72 | case "fr-FR": //French (France)
73 | return Locale.French;
74 |
75 | case "de-AT": //German (Austria)
76 | case "de-CH": //German (Switzerland)
77 | case "de-DE": //German (Germany)
78 | return Locale.German;
79 |
80 | case "pt-BR": //Portuguese (Brazil)
81 | case "pt-PT": //Portuguese (Portugal)
82 | return Locale.Portuguese;
83 |
84 | case "it-CH": //Italian (Switzerland)
85 | case "it-IT": //Italian (Italy)
86 | return Locale.Italian;
87 |
88 | case "es-419": //Spanish (Latin America)
89 | return Locale.Spanish;
90 |
91 | default:
92 | //default to English
93 | return Locale.English;
94 | }
95 | }
96 |
97 | function stringsFile() {
98 | return fs.readFileSync(stringsFilePath(), "utf8");
99 | }
100 |
101 | function stringsFilePath(): string {
102 | return path.join(resourcesPath(), "strings.json");
103 | }
104 |
--------------------------------------------------------------------------------
/src/common/platform.ts:
--------------------------------------------------------------------------------
1 | import { platform } from "os";
2 |
3 | export enum Platform {
4 | NIX,
5 | WIN,
6 | MAC,
7 | }
8 |
9 | export function isMac(): boolean {
10 | return getPlatform() == Platform.MAC;
11 | }
12 | export function isWindows(): boolean {
13 | return getPlatform() == Platform.WIN;
14 | }
15 | export function isLinux(): boolean {
16 | return getPlatform() == Platform.NIX;
17 | }
18 |
19 | export function getPlatform(): Platform {
20 | const currentPlatform = platform();
21 |
22 | switch (currentPlatform) {
23 | case "aix":
24 | case "freebsd":
25 | case "linux":
26 | case "openbsd":
27 | case "android":
28 | case "sunos":
29 | return Platform.NIX;
30 | case "darwin":
31 | return Platform.MAC;
32 | case "win32":
33 | return Platform.WIN;
34 | default:
35 | throw `Did not recognize platform ${currentPlatform}`;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/common/resources.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { isProd } from "./env";
3 |
4 | const ICON_FILENAME = "icon.png";
5 | const CHECKMARK_FILENAME = "check.png";
6 |
7 | const DevResourcesDirName = ".resources";
8 |
9 | export function resourcesPath(): string {
10 | return isProd()
11 | ? process.resourcesPath
12 | : path.join(process.cwd(), DevResourcesDirName);
13 | }
14 |
15 | export function iconPath(): string {
16 | const basePath = path.join(resourcesPath(), ICON_FILENAME);
17 | // Fix for Linux
18 | const pathFixed = basePath.replace(/\\/g, "\\\\");
19 |
20 | return pathFixed;
21 | }
22 |
23 | export function checkmarkPath(): string {
24 | return path.join(resourcesPath(), CHECKMARK_FILENAME);
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/app_setup.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from "electron";
2 | import {
3 | currentBrowserWindow,
4 | restoreWindowAndFocus,
5 | } from "../common/browser_window";
6 | import { createMainWindow } from "./window_setup";
7 | import { isWindows } from "../common/platform";
8 | import { fileOpen } from "./file_open";
9 |
10 | function preventMultipleAppInstances(): void {
11 | if (!app.requestSingleInstanceLock()) {
12 | app.quit();
13 | }
14 | }
15 |
16 | function openMinimizedIfAlreadyExists(
17 | browserWindow: BrowserWindow | null
18 | ): void {
19 | app.on(
20 | "second-instance",
21 | (_event: Event, argv: string[], _workingDirectory: string) => {
22 | console.log(argv);
23 | if (isWindows() && argv.length > 0 && argv.includes("--open-file")) {
24 | fileOpen(browserWindow);
25 | return;
26 | }
27 |
28 | restoreWindowAndFocus(browserWindow);
29 | }
30 | );
31 | }
32 |
33 | function quitOnWindowsAllClosed(): void {
34 | app.on("window-all-closed", () => {
35 | app.quit();
36 | });
37 | }
38 |
39 | function createWindowOnActivate(browserWindow: BrowserWindow | null): void {
40 | app.on("activate", () => {
41 | browserWindow = currentBrowserWindow(browserWindow);
42 | if (!browserWindow) {
43 | browserWindow = createMainWindow();
44 | }
45 | });
46 | }
47 |
48 | export function setupApp(browserWindow: BrowserWindow | null): void {
49 | preventMultipleAppInstances();
50 | openMinimizedIfAlreadyExists(browserWindow);
51 | quitOnWindowsAllClosed();
52 | createWindowOnActivate(browserWindow);
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/context_menu.ts:
--------------------------------------------------------------------------------
1 | import { app, Menu, MenuItem, BrowserWindow } from "electron";
2 | import { i18n } from "./i18n";
3 |
4 | function buildMenu(canCopy: boolean): Menu {
5 | const menu = new Menu();
6 |
7 | if (canCopy) {
8 | menu.append(
9 | new MenuItem({
10 | label: i18n("contextmenu.copy"),
11 | role: "copy",
12 | visible: canCopy,
13 | enabled: canCopy,
14 | })
15 | );
16 | }
17 | menu.append(
18 | new MenuItem({ label: i18n("contextmenu.select-all"), role: "selectAll" })
19 | );
20 | return menu;
21 | }
22 |
23 | export function setupContextMenu(): void {
24 | app.on(
25 | "browser-window-created",
26 | (event: Event, browserWindow: BrowserWindow) => {
27 | browserWindow.webContents.on(
28 | "context-menu",
29 | (_event: Event, params: Electron.ContextMenuParams) => {
30 | const isTextSelected = params.selectionText.trim().length > 0;
31 | buildMenu(params.editFlags.canCopy && isTextSelected).popup();
32 | }
33 | );
34 | }
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/dock.ts:
--------------------------------------------------------------------------------
1 | import { app, ipcMain, BrowserWindow, nativeImage } from "electron";
2 | import { defaultBrowserWindow } from "../common/browser_window";
3 | import { isMac, isWindows } from "../common/platform";
4 | import { checkmarkPath } from "../common/resources";
5 |
6 | export const EVENT_FILES_ADDED = "files-added";
7 | export const EVENT_FILE_PROCESSED = "file-processed";
8 | export const EVENT_ALL_FILES_PROCESSED = "all-files-processed";
9 |
10 | let batchCount = 0;
11 | let remainingCount = 0;
12 |
13 | export function setupDockEventHandlers(
14 | browserWindow: BrowserWindow | null
15 | ): void {
16 | ipcMain.on(EVENT_FILES_ADDED, (_event, filesCount) => {
17 | storeBatchCount(filesCount);
18 |
19 | updateDockAndProgressBar(browserWindow);
20 | windowsOverlayIcon(browserWindow, false);
21 | });
22 |
23 | ipcMain.on(EVENT_FILE_PROCESSED, (_event, _arg) => {
24 | storeFilesCount(remainingCount - 1);
25 |
26 | // if there are none remaining, let the all finished
27 | // event take care of it so we don't double up
28 | if (remainingCount > 0) {
29 | updateDockAndProgressBar(browserWindow);
30 | }
31 | });
32 |
33 | ipcMain.on(EVENT_ALL_FILES_PROCESSED, (_event, _arg) => {
34 | storeBatchCount(0);
35 |
36 | updateDockAndProgressBar(browserWindow);
37 | updateDockBounce(browserWindow);
38 | windowsFlashFrame(browserWindow);
39 | windowsOverlayIcon(browserWindow, true);
40 | });
41 | }
42 |
43 | function storeBatchCount(filesCount: number) {
44 | batchCount = filesCount;
45 | storeFilesCount(batchCount);
46 | }
47 |
48 | function storeFilesCount(filesCount: number): void {
49 | remainingCount = filesCount > 0 ? filesCount : 0;
50 | }
51 |
52 | function updateDockAndProgressBar(browserWindow: BrowserWindow | null) {
53 | updateDockCount();
54 | updateProgressBar(browserWindow);
55 | }
56 |
57 | function updateDockCount(): void {
58 | if (!isMac()) {
59 | return;
60 | }
61 |
62 | if (!app.dock) {
63 | throw new Error("Could not get a handle on the Mac Dock");
64 | }
65 |
66 | // update badge count
67 | app.dock.setBadge(remainingCount > 0 ? remainingCount.toString() : "");
68 | }
69 |
70 | function updateProgressBar(browserWindow: BrowserWindow | null): void {
71 | browserWindow = defaultBrowserWindow(null);
72 | let ratio =
73 | remainingCount <= 0 ? -1 : (batchCount - remainingCount) / batchCount;
74 |
75 | browserWindow.setProgressBar(ratio);
76 | }
77 |
78 | function updateDockBounce(browserWindow: BrowserWindow | null): void {
79 | if (!isMac()) {
80 | return;
81 | }
82 | browserWindow = defaultBrowserWindow(null);
83 | if (browserWindow.isFocused()) {
84 | // don't bother if the window is already focused
85 | return;
86 | }
87 |
88 | app.dock.bounce("critical");
89 | }
90 |
91 | // Window is flashed to inform the user that the window requires
92 | // attention but that it does not currently have the keyboard focus.
93 | // https://www.electronjs.org/docs/tutorial/windows-taskbar#flash-frame
94 | function windowsFlashFrame(browserWindow: BrowserWindow | null): void {
95 | if (!isWindows()) {
96 | return;
97 | }
98 | browserWindow = defaultBrowserWindow(browserWindow);
99 | if (browserWindow.isFocused()) {
100 | // don't bother if the window is already focused
101 | return;
102 | }
103 |
104 | browserWindow.flashFrame(true);
105 | }
106 |
107 | function windowsOverlayIcon(
108 | browserWindow: BrowserWindow | null,
109 | enabled: boolean
110 | ): void {
111 | if (!isWindows()) {
112 | return;
113 | }
114 | browserWindow = defaultBrowserWindow(browserWindow);
115 |
116 | const icon = enabled ? nativeImage.createFromPath(checkmarkPath()) : null;
117 |
118 | browserWindow.setOverlayIcon(icon, "Finished processing all files");
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/file_open.ts:
--------------------------------------------------------------------------------
1 | import { dialog, BrowserWindow } from "electron";
2 | import {
3 | defaultBrowserWindow,
4 | restoreWindowAndFocus,
5 | } from "../common/browser_window";
6 |
7 | export const EVENT_FILE_OPEN_ADD_FILES = "file-open-add-files";
8 |
9 | export function fileOpen(
10 | browserWindow: BrowserWindow | undefined | null
11 | ): void {
12 | browserWindow = defaultBrowserWindow(browserWindow);
13 | restoreWindowAndFocus(browserWindow);
14 |
15 | dialog
16 | .showOpenDialog(browserWindow, {
17 | properties: ["openFile", "multiSelections"],
18 | })
19 | .then((result) => {
20 | if (result.filePaths) {
21 | defaultBrowserWindow(browserWindow).webContents.send(
22 | EVENT_FILE_OPEN_ADD_FILES,
23 | result.filePaths
24 | );
25 | }
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/i18n.ts:
--------------------------------------------------------------------------------
1 | import { app, ipcMain } from "electron";
2 | import { i18n as i18nCommon } from "../common/i18n";
3 |
4 | export const IPC_EVENT_NAME_GET_LOCALE = "get-locale";
5 |
6 | export function i18n(key: string): string {
7 | return i18nCommon(key, locale());
8 | }
9 |
10 | export function setupI18nHandlers() {
11 | ipcMain.handle(IPC_EVENT_NAME_GET_LOCALE, async (_event, _path) => {
12 | return locale();
13 | });
14 | }
15 |
16 | function locale() {
17 | return app.getLocale();
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from "electron";
2 | import { isDev } from "../common/env";
3 |
4 | // electron-webpack HMR for development
5 | if (isDev() && module.hot) {
6 | module.hot.accept();
7 | }
8 |
9 | import { app } from "electron";
10 | import { setupMenus } from "./menu";
11 | import { init } from "./init";
12 | import { createMainWindow, setupMainWindow } from "./window_setup";
13 | import { currentBrowserWindow } from "../common/browser_window";
14 |
15 | // Maintain reference to window to
16 | // prevent it from being garbage collected
17 | var browserWindow = null as BrowserWindow | null;
18 |
19 | async function setup(): Promise {
20 | init(browserWindow);
21 | await app.whenReady();
22 | setupMenus();
23 |
24 | // keep reference to main window to prevent losing it on GC
25 | browserWindow = currentBrowserWindow(browserWindow);
26 | if (!browserWindow) {
27 | browserWindow = createMainWindow();
28 | }
29 | setupMainWindow(browserWindow);
30 | }
31 |
32 | setup();
33 |
--------------------------------------------------------------------------------
/src/main/init.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from "electron";
2 | import packageJson from "../../package.json";
3 | import { setupApp } from "./app_setup";
4 | import { setupContextMenu } from "./context_menu";
5 | import { setupDockEventHandlers } from "./dock";
6 | import { preloadI18nStrings } from "../common/i18n";
7 | import { setupI18nHandlers } from "../main/i18n";
8 |
9 | function setupUserModelId(): void {
10 | app.setAppUserModelId(packageJson.build.appId);
11 | }
12 |
13 | export function init(browserWindow: BrowserWindow | null): void {
14 | preloadI18nStrings();
15 | setupI18nHandlers();
16 | setupContextMenu();
17 | setupDockEventHandlers(browserWindow);
18 | setupUserModelId();
19 | setupApp(browserWindow);
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
1 | import { app, Menu, MenuItemConstructorOptions } from "electron";
2 | import { isMac, isWindows } from "../common/platform";
3 | import { appMenuTemplate } from "./menu_app";
4 | import { dockMenuTemplate } from "./menu_dock";
5 | import { editMenuTemplate } from "./menu_edit";
6 | import { fileMenuTemplate } from "./menu_file";
7 | import { helpMenuTemplate } from "./menu_help";
8 | import { viewMenuTemplate } from "./menu_view";
9 | import { windowMenuTemplate } from "./menu_window";
10 | import { i18n } from "./i18n";
11 |
12 | const APP_ARG_WINDOWS_TASK_OPEN_FILE = "--open-file";
13 |
14 | function menuTemplate(): MenuItemConstructorOptions[] {
15 | return [
16 | ...(isMac() ? [appMenuTemplate()] : []),
17 | fileMenuTemplate(),
18 | editMenuTemplate(),
19 | viewMenuTemplate(),
20 | windowMenuTemplate(),
21 | helpMenuTemplate(),
22 | ];
23 | }
24 |
25 | function menu(): Menu {
26 | return Menu.buildFromTemplate(menuTemplate());
27 | }
28 |
29 | function dockMenu(): Menu {
30 | return Menu.buildFromTemplate(dockMenuTemplate());
31 | }
32 |
33 | function setupMainMenu(): void {
34 | Menu.setApplicationMenu(menu());
35 | }
36 |
37 | function setupDockMenu(): void {
38 | if (!isMac()) {
39 | return;
40 | }
41 |
42 | app.dock.setMenu(dockMenu());
43 | }
44 |
45 | function setupUserTasksMenu(): void {
46 | if (!isWindows()) {
47 | return;
48 | }
49 |
50 | app.setUserTasks([
51 | {
52 | program: process.execPath,
53 | arguments: APP_ARG_WINDOWS_TASK_OPEN_FILE,
54 | iconPath: process.execPath,
55 | iconIndex: 0,
56 | title: i18n("usertasks:open-file.label"),
57 | description: i18n("usertasks:open-file.description"),
58 | },
59 | ]);
60 | }
61 |
62 | export function setupMenus(): void {
63 | setupMainMenu();
64 | setupDockMenu();
65 | setupUserTasksMenu();
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/menu_app.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions, app } from "electron";
2 | import { i18n } from "./i18n";
3 | import { isMac } from "../common/platform";
4 |
5 | export function appMenuTemplate(): MenuItemConstructorOptions {
6 | return {
7 | label: app.getName(),
8 | submenu: [
9 | {
10 | label: `${i18n("menu.app.about")}${app.getName()}`,
11 | role: "about",
12 | },
13 | {
14 | type: "separator",
15 | },
16 | {
17 | label: i18n("menu.app.services"),
18 | role: "services",
19 | },
20 | {
21 | type: "separator",
22 | },
23 | {
24 | label: `${i18n("menu.app.hide")} ${app.getName()}`,
25 | role: "hide",
26 | },
27 | {
28 | label: i18n("menu.app.hide-others"),
29 | role: "hideOthers",
30 | },
31 | {
32 | label: i18n("menu.app.show-all"),
33 | role: "unhide",
34 | },
35 | {
36 | type: "separator",
37 | },
38 | {
39 | label: isMac()
40 | ? `${i18n("menu.app.quit")} ${app.getName()}`
41 | : i18n("menu.app.quit"),
42 | role: "quit",
43 | },
44 | ],
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/menu_app_about.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import { iconPath } from "../common/resources";
3 | import { i18n } from "./i18n";
4 |
5 | export function showAboutWindow(author: string, websiteUrl: string): void {
6 | let aboutPanelOptions = {
7 | applicationName: app.getName(),
8 | applicationVersion: app.getVersion(),
9 | copyright: `${i18n("aboutwindow:copyright")} © ${author}`,
10 | version: app.getVersion(),
11 | credits: author,
12 | authors: [author],
13 | website: websiteUrl,
14 | iconPath: iconPath(),
15 | };
16 |
17 | app.setAboutPanelOptions(aboutPanelOptions);
18 | app.showAboutPanel();
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/menu_dock.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { fileMenuOpenItem } from "./menu_file_open";
3 |
4 | export function dockMenuTemplate(): MenuItemConstructorOptions[] {
5 | return [fileMenuOpenItem()];
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/menu_edit.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { i18n } from "./i18n";
3 | import { isMac } from "../common/platform";
4 |
5 | export function editMenuTemplate(): MenuItemConstructorOptions {
6 | return {
7 | label: i18n("menu.edit.name"),
8 | submenu: [
9 | {
10 | label: i18n("menu.edit.copy"),
11 | role: "copy",
12 | },
13 | {
14 | label: i18n("menu.edit.select-all"),
15 | role: "selectAll",
16 | },
17 | ...(isMac() ? macSubmenu() : []),
18 | ],
19 | };
20 | }
21 |
22 | function macSubmenu(): MenuItemConstructorOptions[] {
23 | return [
24 | {
25 | type: "separator",
26 | },
27 | {
28 | label: i18n("menu.edit.speech"),
29 | submenu: [
30 | {
31 | label: i18n("menu.edit.speech.start-speaking"),
32 | role: "startSpeaking",
33 | },
34 | {
35 | label: i18n("menu.edit.speech.stop-speaking"),
36 | role: "stopSpeaking",
37 | },
38 | ],
39 | },
40 | ];
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/menu_file.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { i18n } from "./i18n";
3 | import { fileMenuOpenItem } from "./menu_file_open";
4 | import { isMac } from "../common/platform";
5 |
6 | export function fileMenuTemplate(): MenuItemConstructorOptions {
7 | return {
8 | label: i18n("menu.file.name"),
9 | role: "fileMenu",
10 | type: "submenu",
11 | submenu: [
12 | fileMenuOpenItem(),
13 | {
14 | type: "separator",
15 | },
16 | fileQuitTemplate(),
17 | ],
18 | };
19 | }
20 |
21 | function fileQuitTemplate(): MenuItemConstructorOptions {
22 | return isMac()
23 | ? {
24 | label: i18n("menu.file.close"),
25 | role: "close",
26 | }
27 | : {
28 | label: i18n("menu.file.quit"),
29 | role: "quit",
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/menu_file_open.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BrowserWindow,
3 | MenuItemConstructorOptions,
4 | MenuItem,
5 | KeyboardEvent,
6 | } from "electron";
7 | import { i18n } from "./i18n";
8 | import { fileOpen } from "./file_open";
9 |
10 | export function fileMenuOpenItem(): MenuItemConstructorOptions {
11 | return {
12 | label: `${i18n("menu.file.open")}…`,
13 | accelerator: "CmdOrCtrl+O",
14 | click: fileOpenClick,
15 | };
16 | }
17 |
18 | function fileOpenClick(
19 | _menuItem: MenuItem,
20 | browserWindow: BrowserWindow | undefined,
21 | _event: KeyboardEvent
22 | ): void {
23 | fileOpen(browserWindow);
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/menu_help.ts:
--------------------------------------------------------------------------------
1 | import { shell, app, MenuItemConstructorOptions } from "electron";
2 | import os from "os";
3 | import { isMac } from "../common/platform";
4 | import { showAboutWindow } from "./menu_app_about";
5 | import { openUrlMenuItem } from "./menu_item_open_url";
6 | import { i18n } from "./i18n";
7 |
8 | const WEBSITE_URL = "https://exifcleaner.com";
9 | const GITHUB_USERNAME = "szTheory";
10 | const GITHUB_PROJECTNAME = "exifcleaner";
11 | const SOURCE_CODE_URL = `https://github.com/${GITHUB_USERNAME}/${GITHUB_PROJECTNAME}`;
12 |
13 | export function helpMenuTemplate(): MenuItemConstructorOptions {
14 | return {
15 | label: i18n("menu.help.name"),
16 | role: "help",
17 | submenu: buildHelpSubmenu(),
18 | };
19 | }
20 |
21 | function buildHelpSubmenu(): MenuItemConstructorOptions[] {
22 | let submenu = [
23 | openUrlMenuItem(i18n("menu.help.website"), WEBSITE_URL),
24 | openUrlMenuItem(i18n("menu.help.source-code"), SOURCE_CODE_URL),
25 | {
26 | label: `${i18n("menu.help.report-issue")}…`,
27 | click() {
28 | const url = newGithubIssueUrl(
29 | GITHUB_USERNAME,
30 | GITHUB_PROJECTNAME,
31 | newGithubIssueBody()
32 | );
33 | shell.openExternal(url);
34 | },
35 | },
36 | ];
37 |
38 | if (!isMac()) {
39 | submenu.push(
40 | {
41 | type: "separator",
42 | },
43 | {
44 | label: `${i18n("menu.help.about")}${app.getName()}`,
45 | click() {
46 | showAboutWindow(GITHUB_USERNAME, WEBSITE_URL);
47 | },
48 | }
49 | );
50 | }
51 |
52 | return submenu;
53 | }
54 |
55 | function newGithubIssueUrl(user: string, repo: string, body: string): string {
56 | const url = new URL(`https://github.com/${user}/${repo}/issues/new`);
57 | url.searchParams.set("body", body);
58 |
59 | return url.toString();
60 | }
61 |
62 | function newGithubIssueBody(): string {
63 | return `
64 |
65 |
66 |
67 | ---
68 |
69 | ${debugInfo()}`;
70 | }
71 |
72 | function debugInfo(): string {
73 | return `
74 | ${app.getName()} ${app.getVersion()}
75 | Electron ${process.versions.electron}
76 | ${process.platform} ${os.release()}
77 | Locale: ${app.getLocale()}
78 | `.trim();
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/menu_item_open_url.ts:
--------------------------------------------------------------------------------
1 | import { shell, MenuItemConstructorOptions } from "electron";
2 |
3 | export function openUrlMenuItem(
4 | label: string,
5 | url: string
6 | ): MenuItemConstructorOptions {
7 | return {
8 | label: label,
9 | click: function () {
10 | shell.openExternal(url);
11 | },
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/menu_view.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { i18n } from "./i18n";
3 |
4 | export function viewMenuTemplate(): MenuItemConstructorOptions {
5 | return {
6 | label: i18n("menu.view.name"),
7 | submenu: [
8 | {
9 | label: i18n("menu.view.toggle-dev-tools"),
10 | role: "toggleDevTools",
11 | },
12 | { type: "separator" },
13 | {
14 | label: i18n("menu.view.zoom-reset"),
15 | role: "resetZoom",
16 | },
17 | {
18 | label: i18n("menu.view.zoom-in"),
19 | role: "zoomIn",
20 | },
21 | {
22 | label: i18n("menu.view.zoom-out"),
23 | role: "zoomOut",
24 | },
25 | { type: "separator" },
26 | {
27 | label: i18n("menu.view.toggle-full-screen"),
28 | role: "togglefullscreen",
29 | },
30 | ],
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/menu_window.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { i18n } from "./i18n";
3 | import { isMac } from "../common/platform";
4 |
5 | export function windowMenuTemplate(): MenuItemConstructorOptions {
6 | return {
7 | label: i18n("menu.window.name"),
8 | submenu: [
9 | {
10 | label: isMac()
11 | ? i18n("menu.window.minimize-mac")
12 | : i18n("menu.window.minimize"),
13 | role: "minimize",
14 | },
15 | {
16 | label: isMac()
17 | ? i18n("menu.window.zoom-mac")
18 | : i18n("menu.window.zoom"),
19 | role: "zoom",
20 | },
21 | ...(isMac() ? macSubmenu() : defaultSubmenu()),
22 | ],
23 | };
24 | }
25 |
26 | function macSubmenu(): MenuItemConstructorOptions[] {
27 | return [
28 | { type: "separator" },
29 | {
30 | label: i18n("menu.window.front"),
31 | role: "front",
32 | },
33 | { type: "separator" },
34 | {
35 | label: i18n("menu.window.window"),
36 | role: "window",
37 | },
38 | ];
39 | }
40 |
41 | function defaultSubmenu(): MenuItemConstructorOptions[] {
42 | return [
43 | {
44 | label: i18n("menu.window.close"),
45 | role: "close",
46 | },
47 | ];
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/window_setup.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, app } from "electron";
2 | import url from "url";
3 | import path from "path";
4 | import { isDev } from "../common/env";
5 | import { isMac, isWindows } from "../common/platform";
6 | import { iconPath } from "../common/resources";
7 |
8 | const DEFAULT_WINDOW_WIDTH = 580;
9 | const DEFAULT_WINDOW_HEIGHT = 312;
10 |
11 | function setupMainWindowClose(browserWindow: BrowserWindow) {
12 | browserWindow.on("closed", () => {
13 | // on Mac, the convention is to leave the app
14 | // open even when all windows are closed. so that for
15 | // example they can relaunch the app from the dock
16 | // or still use the drag to dock features
17 | if (!isMac()) {
18 | // quit application on window close
19 | app.quit();
20 | }
21 | });
22 | }
23 |
24 | function showWindowOnReady(browserWindow: BrowserWindow) {
25 | browserWindow.once("ready-to-show", () => {
26 | browserWindow.show();
27 | browserWindow.focus();
28 | });
29 | }
30 |
31 | // On Windows, stop flashing the frame once the window comes into focus.
32 | // More: https://www.electronjs.org/docs/tutorial/windows-taskbar#flash-frame
33 | function windowsStopFlashingFrameOnFocus(browserWindow: BrowserWindow) {
34 | if (!isWindows()) {
35 | return;
36 | }
37 |
38 | browserWindow.once("focus", () => browserWindow.flashFrame(false));
39 | }
40 |
41 | function urlForLoad() {
42 | if (isDev()) {
43 | const port = process.env.ELECTRON_WEBPACK_WDS_PORT;
44 | if (!port) {
45 | throw "No Electron webpack WDS port set for dev. Try running `yarn run dev` instead for development mode.";
46 | }
47 |
48 | return `http://localhost:${port}`;
49 | } else {
50 | return url.format({
51 | pathname: path.join(__dirname, "index.html"),
52 | protocol: "file",
53 | slashes: true,
54 | });
55 | }
56 | }
57 |
58 | function mainWindowLoadUrl(browserWindow: BrowserWindow) {
59 | browserWindow.loadURL(urlForLoad());
60 | }
61 |
62 | const WINDOW_BACKGROUND_COLOR = "#F5F6F8";
63 |
64 | export function createMainWindow(): BrowserWindow {
65 | let options = {
66 | title: app.name,
67 | show: false,
68 | width: DEFAULT_WINDOW_WIDTH,
69 | height: DEFAULT_WINDOW_HEIGHT + 25,
70 | minWidth: DEFAULT_WINDOW_WIDTH,
71 | minHeight: DEFAULT_WINDOW_HEIGHT + 25,
72 | webPreferences: {
73 | nodeIntegration: true,
74 | // TODO: need to get this working with "true" to upgrade to Electron 12,
75 | // but electron-webpack depends on it being "false" and it's been abandonded
76 | // contextIsolation: true,
77 | },
78 | //set specific background color eliminate white flicker on content load
79 | backgroundColor: WINDOW_BACKGROUND_COLOR,
80 | icon: iconPath(),
81 | };
82 |
83 | return new BrowserWindow(options);
84 | }
85 |
86 | export function setupMainWindow(browserWindow: BrowserWindow): void {
87 | setupMainWindowClose(browserWindow);
88 | // load URL before showing the window to avoid flash of unloaded content
89 | mainWindowLoadUrl(browserWindow);
90 | showWindowOnReady(browserWindow);
91 | windowsStopFlashingFrameOnFocus(browserWindow);
92 | }
93 |
--------------------------------------------------------------------------------
/src/renderer/add_files.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from "electron";
2 | import { ExiftoolProcess } from "node-exiftool";
3 | import { EVENT_FILE_PROCESSED, EVENT_FILES_ADDED } from "../main/dock";
4 | import { displayExifBeforeClean, displayExifAfterClean } from "./display_exif";
5 | import { removeExif } from "./exif_remove";
6 | import { addTableRow } from "./table_add_row";
7 | import { updateRowWithCleanerSpinner } from "./table_update_row";
8 |
9 | export async function addFiles(
10 | filePaths: string[],
11 | exifToolProcesses: ExiftoolProcess[]
12 | ): Promise {
13 | ipcRenderer.send(EVENT_FILES_ADDED, filePaths.length);
14 |
15 | const filePathsIterator = filePath(filePaths);
16 |
17 | const promises = exifToolProcesses.map((exifToolProcess) => {
18 | return processFile(
19 | filePathsIterator,
20 | exifToolProcess,
21 | exifToolProcess.open()
22 | ).catch(() => {
23 | exifToolProcess.close();
24 | });
25 | });
26 |
27 | return Promise.all(promises);
28 | }
29 |
30 | async function processFile(
31 | filePathsIterator: Generator,
32 | exifToolProcess: ExiftoolProcess,
33 | exifToolPromise: Promise
34 | ): Promise {
35 | return exifToolPromise.then(() => {
36 | const iteratorResult = filePathsIterator.next();
37 | if (iteratorResult.done) {
38 | return exifToolProcess.close();
39 | }
40 |
41 | const filePath = iteratorResult.value;
42 | const promise = addFile(filePath, exifToolProcess);
43 |
44 | return processFile(filePathsIterator, exifToolProcess, promise);
45 | });
46 | }
47 |
48 | function* filePath(filePaths: string[]): Generator {
49 | for (const filePath of filePaths) {
50 | yield filePath;
51 | }
52 | }
53 |
54 | async function addFile(
55 | filePath: string,
56 | exifToolProcess: ExiftoolProcess
57 | ): Promise {
58 | const tableRow = addTableRow(filePath);
59 |
60 | return displayExifBeforeClean(exifToolProcess, tableRow, filePath)
61 | .then((_exifData) => {
62 | updateRowWithCleanerSpinner(tableRow);
63 |
64 | return removeExif(exifToolProcess, filePath);
65 | })
66 | .then((_stdOutErrOutput) => {
67 | ipcRenderer.send(EVENT_FILE_PROCESSED);
68 |
69 | return displayExifAfterClean(exifToolProcess, tableRow, filePath);
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/src/renderer/display_exif.ts:
--------------------------------------------------------------------------------
1 | import { ExiftoolProcess } from "node-exiftool";
2 | import { getExif } from "./exif_get";
3 | import { updateRowWithExif } from "./table_update_row";
4 |
5 | export async function displayExifBeforeClean(
6 | exifToolProcess: ExiftoolProcess,
7 | trNode: HTMLTableRowElement,
8 | filePath: string
9 | ): Promise {
10 | const tdBeforeNode = trNode.querySelector("td:nth-child(2)");
11 | if (!(tdBeforeNode instanceof HTMLTableCellElement)) {
12 | throw new Error("Expected table data cell element");
13 | }
14 |
15 | return getExif(exifToolProcess, filePath).then((exifData) => {
16 | updateRowWithExif(tdBeforeNode, exifData);
17 |
18 | return exifData;
19 | });
20 | }
21 |
22 | export async function displayExifAfterClean(
23 | exifToolProcess: ExiftoolProcess,
24 | trNode: HTMLTableRowElement,
25 | filePath: string
26 | ): Promise {
27 | const tdAfterNode = trNode.querySelector("td:nth-child(3)");
28 | if (!(tdAfterNode instanceof HTMLTableCellElement)) {
29 | throw new Error("Expected table data cell element");
30 | }
31 |
32 | return getExif(exifToolProcess, filePath).then((exifDataAfterClean) => {
33 | updateRowWithExif(tdAfterNode, exifDataAfterClean);
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/renderer/drag.ts:
--------------------------------------------------------------------------------
1 | import { selectFiles } from "./select_files";
2 |
3 | export function setupDragAndDrop(): void {
4 | document.addEventListener("drop", (event) => {
5 | handleDropEvent(event);
6 | });
7 |
8 | document.addEventListener("dragover", (event) => {
9 | handleDragOverEvent(event);
10 | });
11 | }
12 |
13 | function handleDropEvent(event: DragEvent): void {
14 | event.preventDefault();
15 | event.stopPropagation();
16 |
17 | const dataTransfer = event.dataTransfer;
18 | if (!dataTransfer) {
19 | throw "Error getting data transfer for drop event";
20 | }
21 | const fileList = dataTransfer.files;
22 | const paths = filePaths(fileList);
23 |
24 | selectFiles(paths);
25 | }
26 |
27 | function handleDragOverEvent(event: DragEvent): void {
28 | event.preventDefault();
29 | event.stopPropagation();
30 | }
31 |
32 | function filePaths(fileList: FileList): string[] {
33 | let paths = [];
34 | for (const file of fileList) {
35 | paths.push(file.path);
36 | }
37 |
38 | return paths;
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/empty_pane.ts:
--------------------------------------------------------------------------------
1 | export function hideEmptyPane(): void {
2 | const pane = emptyPane();
3 | if (!pane) {
4 | throw "Could not find empty pane to hide";
5 | }
6 |
7 | pane.classList.add("d-none");
8 | }
9 |
10 | function emptyPane(): HTMLElement | null {
11 | return document.getElementById("empty");
12 | }
13 |
--------------------------------------------------------------------------------
/src/renderer/exif_get.ts:
--------------------------------------------------------------------------------
1 | import { ExiftoolProcess } from "node-exiftool";
2 |
3 | const EXIFTOOL_ARGS_GET_EXIF = [
4 | "charset filename=UTF8",
5 | "-File:all",
6 | "-ExifToolVersion",
7 | ];
8 |
9 | // Read exif data using the ExifTool binary
10 | // and clean up after the process when done
11 | export async function getExif(
12 | exiftoolProcess: ExiftoolProcess,
13 | filePath: string
14 | ): Promise