├── .gitignore ├── LICENSE ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── gh-deploy.sh ├── start.js └── test.js ├── src ├── App.css ├── App.js ├── StringSearcher.js ├── StringWriter.js ├── components │ ├── Button │ │ ├── Button.css │ │ └── Button.js │ ├── FileSelector │ │ ├── FileSelector.css │ │ └── FileSelector.js │ ├── SettingsPanel │ │ ├── CheckboxOption.js │ │ ├── InputOption.js │ │ ├── SelectorOption.js │ │ ├── SettingsPanel.css │ │ └── SettingsPanel.js │ ├── StringList │ │ ├── StringEntry.css │ │ ├── StringEntry.js │ │ ├── StringList.css │ │ └── StringList.js │ └── index.js ├── i18n │ ├── i18n.js │ ├── langs │ │ ├── en.json │ │ └── pt-BR.json │ └── string-template.js ├── icons │ ├── coffee.js │ ├── gear.js │ ├── info.js │ ├── settings-close.js │ ├── settings-open.js │ └── upload.js ├── index.js ├── settings.js └── util │ ├── descriptor-parser.js │ ├── registerServiceWorker.js │ ├── string-util.js │ └── util.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | TODO.txt 24 | gh-deploy 25 | 26 | public/*.jar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jar-string-editor 2 | 3 | 4 | ##### Rodando localmente: 5 | Requisitos: `node`, `yarn (ou npm)` 6 | - Clone o repositório usando: `git clone https://github.com/leonardosnt/jar-string-editor` 7 | - Vá até o diretório usando: `cd jar-string-editor` 8 | - Instale as dependências usando: `yarn` 9 | - Inicie o servidor de testes usando: `yarn start` 10 | - Editar o código e o site será automaticamente recarregado. 11 | 12 | ## License 13 | Copyright (C) leonardosnt 14 | Licensed under the GPL-2.0 license. See LICENSE file in the project root for full license information. -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | 78 | // Build info 79 | __BUILD_INFO__: JSON.stringify({ 80 | date: new Date().toISOString(), 81 | commit: (() => { 82 | if (process.env.NODE_ENV === 'development') { 83 | return 'development'; 84 | } 85 | 86 | console.log('fetching lastest commit...'); 87 | 88 | const cp = require('child_process'); 89 | const { stderr, stdout } = cp.spawnSync('git', [ 90 | 'log', 91 | '-n', 92 | '1', 93 | '--pretty=format:%H', 94 | ]); 95 | 96 | if (stderr.toString()) { 97 | console.log('failed to get last commit.'); 98 | console.log(stderr); 99 | } 100 | 101 | const commitHash = stdout.toString(); 102 | 103 | console.log('latest commit hash:', commitHash); 104 | 105 | return commitHash; 106 | })(), 107 | }), 108 | } 109 | ); 110 | // Stringify all values so we can feed into Webpack DefinePlugin 111 | const stringified = { 112 | 'process.env': Object.keys(raw).reduce((env, key) => { 113 | env[key] = JSON.stringify(raw[key]); 114 | return env; 115 | }, {}), 116 | }; 117 | 118 | return { raw, stringified }; 119 | } 120 | 121 | module.exports = getClientEnvironment; 122 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 49 | 50 | 60 | <% } %> 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Jar String Editor", 3 | "name": "Jar String Editor", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/gh-deploy.sh: -------------------------------------------------------------------------------- 1 | cd ../gh-deploy 2 | 3 | export PUBLIC_URL="https://leonardosnt.github.io/jar-string-editor/" 4 | 5 | echo Building... 6 | 7 | yarn build 8 | 9 | rm -rf static # remove old assets 10 | cp -rfv ../build/* ./ 11 | 12 | echo =========== Modified files =========== 13 | git diff --name-only 14 | echo ============================================ 15 | 16 | echo Commit changes [y/n] ? 17 | 18 | read shouldCommit 19 | 20 | if [ $shouldCommit == "y" ]; then 21 | git add . 22 | git commit -m "update" 23 | git push -u origin gh-pages 24 | fi 25 | 26 | read -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | // We attempt to use the default port but if it is busy, we offer the user to 47 | // run on a different port. `detect()` Promise resolves to the next free port. 48 | choosePort(HOST, DEFAULT_PORT) 49 | .then(port => { 50 | if (port == null) { 51 | // We have not found a port. 52 | return; 53 | } 54 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 55 | const appName = require(paths.appPackageJson).name; 56 | const urls = prepareUrls(protocol, HOST, port); 57 | // Create a webpack compiler that is configured with custom messages. 58 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 59 | // Load proxy config 60 | const proxySetting = require(paths.appPackageJson).proxy; 61 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 62 | // Serve webpack assets generated by the compiler over a web sever. 63 | const serverConfig = createDevServerConfig( 64 | proxyConfig, 65 | urls.lanUrlForConfig 66 | ); 67 | const devServer = new WebpackDevServer(compiler, serverConfig); 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | if (isInteractive) { 74 | clearConsole(); 75 | } 76 | console.log(chalk.cyan('Starting the development server...\n')); 77 | //openBrowser(urls.localUrlForBrowser); 78 | }); 79 | 80 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 81 | process.on(sig, function() { 82 | devServer.close(); 83 | process.exit(); 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | if (err && err.message) { 89 | console.log(err.message); 90 | } 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | jest.run(argv); 27 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | margin: 0 50px 0 50px; 4 | flex-direction: column; 5 | } 6 | 7 | @media (min-width: 300px) and (max-width: 800px) { 8 | .header { 9 | margin: 0 10px 0 10px; 10 | } 11 | } 12 | 13 | .brand-container { 14 | margin-top: 20px; 15 | margin-bottom: 20px; 16 | user-select: none; 17 | display: flex; 18 | justify-content: center; 19 | } 20 | 21 | .brand { 22 | color: rgba(0, 0, 0, 0.8); 23 | cursor: pointer; 24 | } 25 | 26 | .search { 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | 31 | .search input { 32 | flex: 1; 33 | height: 20px; 34 | font-size: 1.05em; 35 | border: 1px solid rgba(0, 0, 0, .3); 36 | } 37 | .search input:focus { 38 | outline: none; 39 | border: 1px solid rgba(0, 0, 0, .7); 40 | } 41 | 42 | .info { 43 | padding-top: 10px; 44 | } 45 | 46 | .save-btn { 47 | float: right; 48 | } 49 | 50 | .load-info-box { 51 | margin: auto; 52 | 53 | width: 50%; 54 | height: 200px; 55 | 56 | background: white; 57 | box-shadow: 0 1px 6px 0 rgba(13, 13, 14, 0.1); 58 | 59 | text-align: center; 60 | padding: 70px; 61 | cursor: pointer; 62 | 63 | user-select: none; 64 | } 65 | .load-info-box p { 66 | padding-top: 10px; 67 | color: rgba(0, 0, 0, 0.8); 68 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | import debounce from "lodash.debounce"; 21 | import JSZip from "jszip"; 22 | 23 | import { Button, SettingsPanel, FileSelector, StringList } from "./components"; 24 | import { 25 | getStringContext, 26 | extractClassName, 27 | extractMethodInfoConstants, 28 | getInstructionLineNumber, 29 | } from "./util/util"; 30 | import { escapeRegExp } from "./util/string-util"; 31 | import { saveAs } from "file-saver"; 32 | import { translate } from "./i18n/i18n"; 33 | 34 | import StringSearcher from "./StringSearcher"; 35 | import StringWriter from "./StringWriter"; 36 | import Settings from "./settings"; 37 | 38 | import GearSvg from "./icons/gear"; 39 | import CoffeIcon from "./icons/coffee"; 40 | 41 | import "./App.css"; 42 | 43 | class App extends Component { 44 | static INITIAL_STATE = Object.freeze({ 45 | loadedJar: undefined, 46 | selectedFileName: undefined, 47 | strings: [], 48 | filter: undefined, 49 | }); 50 | 51 | state = { ...App.INITIAL_STATE } 52 | 53 | constructor(props) { 54 | super(props); 55 | 56 | const setupSearchDebounce = () => { 57 | if (this.debouncedOnSearchChange) { 58 | this.debouncedOnSearchChange.cancel(); 59 | } 60 | 61 | this.debouncedOnSearchChange = debounce( 62 | filter => this.setState({ filter }), 63 | Number(Settings.debounceRate) 64 | ); 65 | }; 66 | 67 | setupSearchDebounce(); 68 | 69 | // Update when settings change 70 | Settings.observe(oldSettings => { 71 | if (oldSettings.sortByContext !== Settings.sortByContext) { 72 | this.setState(state => ({ strings: this._sortByContext(state.strings) })); 73 | } 74 | if (oldSettings.debounceRate !== Settings.debounceRate) { 75 | setupSearchDebounce(); 76 | } 77 | 78 | this.forceUpdate(); 79 | }); 80 | } 81 | 82 | clearContext = () => { 83 | if (this.currentStringSearcher) { 84 | this.currentStringSearcher.stop(); 85 | } 86 | 87 | this.selectedFile = undefined; 88 | this.jdecWindow = undefined; 89 | 90 | this.setState(App.INITIAL_STATE); 91 | }; 92 | 93 | onSaveFile = ({ target }) => { 94 | const {loadedJar, strings, selectedFileName} = this.state; 95 | 96 | // Disable button while saving 97 | target.disabled = true; 98 | 99 | StringWriter.write(loadedJar, strings) 100 | .then(blob => { 101 | saveAs(blob, selectedFileName || "Translated.jar"); 102 | }) 103 | .then(() => (target.disabled = false)); 104 | 105 | gaEvent("file", "save"); 106 | }; 107 | 108 | onJarLoaded = (jar, selectedFileName) => { 109 | const stringSearcher = new StringSearcher(); 110 | const numClasses = jar.filter(path => path.endsWith(".class")).length; 111 | 112 | // Used to stop the current search if needed. 113 | this.currentStringSearcher = stringSearcher; 114 | 115 | this.setState({ 116 | loadInfo: translate("app.collecting_strings", { 117 | progress: 0, 118 | numDone: 0, 119 | numClasses, 120 | }), 121 | loadedJar: jar, 122 | selectedFileName: selectedFileName, 123 | }); 124 | 125 | stringSearcher.on("read_count", numDone => { 126 | this.setState({ 127 | loadInfo: translate("app.collecting_strings", { 128 | progress: (numDone / numClasses * 100).toFixed(1), 129 | numDone, 130 | numClasses, 131 | }), 132 | }); 133 | }); 134 | 135 | stringSearcher.on("finish", this.onStringSearcherFinish); 136 | 137 | stringSearcher.searchInJar(jar); 138 | }; 139 | 140 | onStringSearcherFinish = result => { 141 | const stringsFound = []; 142 | let stringId = 0; 143 | 144 | for (const { classFile, fileName, methods } of result) { 145 | const className = extractClassName( 146 | classFile.this_class, 147 | classFile.constant_pool 148 | ); 149 | 150 | for (const { method, strings, instructions } of methods) { 151 | // Extract method info constants only once 152 | const methodLocation = extractMethodInfoConstants( 153 | method, 154 | classFile.constant_pool 155 | ); 156 | 157 | for (const string of strings) { 158 | const location = { 159 | className, 160 | method: methodLocation, 161 | lineNumber: getInstructionLineNumber(classFile, method, string.instruction), 162 | }; 163 | 164 | stringsFound.push({ 165 | ...string, 166 | fileName, 167 | location, 168 | id: stringId++, 169 | context: Settings.sortByContext 170 | && getStringContext(classFile.constant_pool, instructions, string.instructionIndex), 171 | }); 172 | } 173 | } 174 | } 175 | 176 | // We don't need this anymore 177 | delete this.currentStringSearcher; 178 | 179 | if (Settings.sortByContext) { 180 | this._sortByContext(stringsFound); 181 | } 182 | 183 | this.setState({ loadInfo: undefined, strings: stringsFound }); 184 | }; 185 | 186 | onFileSelected = file => { 187 | gaEvent("file", "select"); 188 | 189 | // TODO: not sure if this should be stored on state... 190 | this.selectedFile = file; 191 | 192 | return JSZip.loadAsync(file).then(jar => this.onJarLoaded(jar, file.name)); 193 | }; 194 | 195 | onSearchChange = (e) => { 196 | if (this.debouncedOnSearchChange) { 197 | this.debouncedOnSearchChange(e.target.value); 198 | } else { 199 | this.setState({ filter: e.target.value }); 200 | } 201 | }; 202 | 203 | onStringChanged = (newValue, stringId) => { 204 | const string = this.state.strings.find(s => s.id === stringId); 205 | 206 | if (newValue !== string.value) { 207 | string.value = newValue; 208 | string.changed = true; 209 | } 210 | }; 211 | 212 | filterStrings = () => { 213 | const {strings, filter} = this.state; 214 | 215 | if (!filter) { 216 | // TODO: maybe we should filter empty strings only when they are loaded or when 217 | // the settings change... 218 | return ({ 219 | filtered: strings.filter(f => !Settings.hideEmptyStrings || f.value.trim().length > 0), 220 | took: 0 221 | }); 222 | } 223 | 224 | const filtered = []; 225 | const words = filter.split(" "); 226 | const filterStart = performance.now(); 227 | 228 | // Regex pattern to match all words in any order 229 | // Based on https://stackoverflow.com/a/4389683 (adapted) 230 | const wordsPattern = words 231 | .map(w => escapeRegExp(w)) 232 | .map(w => `(?=.*${w})`) 233 | .join(''); 234 | const regex = new RegExp(`^${wordsPattern}.*$`, 'i'); 235 | 236 | for (const string of strings) { 237 | if (Settings.hideEmptyStrings && string.value.trim().length === 0) continue; 238 | 239 | if (regex.test(string.value)) { 240 | filtered.push(Object.assign({ highlightWords: words }, string)); 241 | } 242 | } 243 | 244 | const filterEnd = performance.now(); 245 | 246 | return { filtered, took: filterEnd - filterStart }; 247 | }; 248 | 249 | handleViewClass = (string) => { 250 | const JDEC_URL = process.env.REACT_APP_JDEC_DEV_URL || "https://jdec.app"; 251 | 252 | const payload = { 253 | action: "open", 254 | jarFile: this.selectedFile, 255 | path: string.fileName, 256 | highlight: string.value 257 | }; 258 | 259 | if (this.jdecWindow === undefined || this.jdecWindow.closed) { 260 | const handleAppReady = e => { 261 | const originHost = new URL(e.origin).host; 262 | const jdecHost = new URL(JDEC_URL).host; 263 | 264 | if (originHost !== jdecHost || e.data !== "app-ready") return; 265 | 266 | window.removeEventListener("message", handleAppReady); 267 | 268 | this.jdecWindow.postMessage(payload, JDEC_URL); 269 | }; 270 | window.addEventListener("message", handleAppReady); 271 | 272 | // TODO: not sure if this should be stored on state... 273 | this.jdecWindow = window.open(`${JDEC_URL}?jse`, "jdec"); 274 | } else { 275 | this.jdecWindow.focus(); 276 | this.jdecWindow.postMessage(payload, JDEC_URL); 277 | } 278 | 279 | gaEvent("misc", "view-class"); 280 | }; 281 | 282 | renderAppContainer = children => ( 283 |
284 | 285 | 286 |
287 |

288 | Jar String Editor 289 |

290 |
291 | 292 | {children} 293 |
294 | ); 295 | 296 | render() { 297 | const { loadInfo, loadedJar, strings } = this.state; 298 | 299 | if (loadedJar === undefined) { 300 | return this.renderAppContainer( 301 |
302 | 303 |
304 |
305 | ); 306 | } 307 | 308 | if (loadInfo) { 309 | return this.renderAppContainer( 310 |
311 | 312 |

{loadInfo}

313 |
314 | ); 315 | } 316 | 317 | const { filtered, took } = this.filterStrings(); 318 | 319 | return this.renderAppContainer( 320 |
321 |
322 |
323 |
{translate("app.search")}
324 | 325 |
326 |
327 | 328 | {translate("app.strings_info", { 329 | took: took.toFixed(2), 330 | found: strings.length, 331 | after_filter: filtered.length, 332 | })} 333 | 334 | 337 |
338 |
339 | 340 | 341 |
342 | ); 343 | } 344 | 345 | _sortByContext = strings => { 346 | const contextPriority = { 347 | SendMessage: 2, 348 | ItemDisplayName: 1, 349 | [undefined]: 0, 350 | }; 351 | 352 | strings.sort((str0, str1) => { 353 | const ctx0 = str0.context; 354 | const ctx1 = str1.context; 355 | return ctx0 === ctx1 ? 0 : contextPriority[ctx1] - contextPriority[ctx0]; 356 | }); 357 | 358 | return strings; 359 | }; 360 | } 361 | 362 | const Link = props => ( 363 | 372 | {props.children} 373 | 374 | ); 375 | 376 | const Footer = () => ( 377 |
385 | {translate("app.created_by", { 386 | coffee: , 387 | link: ( 388 | 389 | leonardosnt 390 | 391 | ), 392 | })} 393 |
394 | ); 395 | 396 | function gaEvent(category, action, label, value) { 397 | if (typeof window.ga === "function") { 398 | window.ga("send", "event", category, action, label, value); 399 | } 400 | } 401 | 402 | window.__BUILD_INFO__ = process.env.__BUILD_INFO__; 403 | 404 | Settings.observe(oldSettings => { 405 | if (oldSettings.language === Settings.language || !window.ga) return; 406 | gaEvent("language", "change", Settings.language); 407 | }); 408 | 409 | window.addEventListener("mousedown", e => document.body.classList.add("using-mouse")); 410 | window.addEventListener("keydown", e => e.key === "Tab" && document.body.classList.remove("using-mouse")); 411 | 412 | export default App; -------------------------------------------------------------------------------- /src/StringSearcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import mitt from "mitt"; 20 | import { 21 | getAttribute, 22 | getUtf8String, 23 | extractMethodInfoConstants, 24 | } from "./util/util"; 25 | import { 26 | JavaClassFileReader, 27 | Modifier, 28 | InstructionParser, 29 | Opcode, 30 | ConstantType, 31 | } from "java-class-tools"; 32 | 33 | export default class StringReader { 34 | constructor() { 35 | Object.assign(this, mitt()); 36 | this._classReader = new JavaClassFileReader(); 37 | this._stopped = false; 38 | this._result = []; 39 | } 40 | 41 | /** 42 | * Stop the searcher 43 | */ 44 | stop() { 45 | this._stopped = true; 46 | } 47 | 48 | /** 49 | * Search in all classes from the jar file. 50 | * 51 | * @param {JSZip} jar - The jar file 52 | */ 53 | searchInJar(jar) { 54 | const classes = jar.filter(path => path.endsWith(".class")); 55 | 56 | /** 57 | * We process the class files sequentially to use less memory (JSZIP uses a lot of memory) 58 | * and to be able to "give feedback to the UI" (i.e. display how many class files have been read). 59 | * 60 | * It's like "promise series" 61 | */ 62 | let currentClassIndex = 0; 63 | 64 | const processNext = () => { 65 | const currentClassFile = classes[currentClassIndex++]; 66 | 67 | // If currentClassFile is undefined, we are done 68 | if (currentClassFile === undefined || this._stopped) { 69 | this.emit("finish", this._result); 70 | return; 71 | } 72 | 73 | // Unzip the class file and search on it 74 | currentClassFile 75 | .async("arraybuffer") 76 | .then(classData => { 77 | try { 78 | this.searchInClass(currentClassFile.name, classData); 79 | } catch (e) { 80 | console.error(`Failed to search in class "${currentClassFile.name}"`); 81 | console.error(e); 82 | } 83 | }) 84 | .then(() => processNext()); 85 | 86 | // Every 100 (currently hardcoded) classes we emit an event 87 | // to update the UI 88 | if (currentClassIndex && currentClassIndex % 100 === 0) { 89 | this.emit("read_count", currentClassIndex); 90 | } 91 | }; 92 | 93 | // Start processing the classes 94 | processNext(); 95 | } 96 | 97 | /** 98 | * Search all strings in a class file. 99 | * 100 | * @param {string} fileName - The file name (only used to indentify where the string was found 101 | * -- we use this to avoid read the class name) 102 | * @param {Buffer|ArrayBuffer|Uint8Array} classData - Class data 103 | */ 104 | searchInClass(fileName, classData) { 105 | const classFile = this._classReader.read(classData); 106 | const constantPool = classFile.constant_pool; 107 | 108 | /** 109 | * Here we store the constantIndex of the strings we already found. 110 | * It's used because the same string can be referenced many times in different methods. 111 | */ 112 | const alreadyMappedStrings = new Set(); 113 | 114 | const stringsByMethod = classFile.methods 115 | .filter(method => (method.access_flags & Modifier.ABSTRACT) === 0) 116 | .map(method => { 117 | const codeAttribute = getAttribute(classFile, method, "Code"); 118 | 119 | if (!codeAttribute || this._isEnumClassInit(classFile, method)) { 120 | return undefined; 121 | } 122 | 123 | const instructions = InstructionParser.fromBytecode(codeAttribute.code); 124 | 125 | const stringsInThisMethod = []; 126 | 127 | for (let i = 0; i < instructions.length; i++) { 128 | const { opcode, operands } = instructions[i]; 129 | 130 | // We only want LDC_W & LDC instructions 131 | if (opcode !== Opcode.LDC && opcode !== Opcode.LDC_W) { 132 | continue; 133 | } 134 | 135 | // The index of the constant in constant_pool. If the opcode is LDC_W, 136 | // the index will be 2 bytes long. 137 | const constantIndex = 138 | opcode === Opcode.LDC 139 | ? operands[0] 140 | : (operands[0] << 8) | operands[1]; // LDC_W 141 | 142 | const constantEntry = constantPool[constantIndex]; 143 | 144 | if (constantEntry.tag !== ConstantType.STRING) { 145 | continue; 146 | } 147 | 148 | // Check if this string was already "found" in this file. 149 | if (alreadyMappedStrings.has(constantIndex)) { 150 | continue; 151 | } 152 | 153 | stringsInThisMethod.push({ 154 | constantIndex, 155 | instruction: instructions[i], 156 | value: getUtf8String(classFile.constant_pool, constantIndex), 157 | instructionIndex: i, 158 | }); 159 | 160 | alreadyMappedStrings.add(constantIndex); 161 | } 162 | 163 | return { method, instructions, strings: stringsInThisMethod }; 164 | }) 165 | .filter(m => m && m.strings.length > 0); 166 | 167 | if (stringsByMethod.length > 0) { 168 | this._result.push({ 169 | classFile, 170 | fileName, 171 | methods: stringsByMethod, 172 | }); 173 | } 174 | } 175 | 176 | /** 177 | * Checks if the method is a class initializer of an Enum. 178 | * 179 | * This is used to ignore strings found inside class initializers in Enums 180 | * because they are usually compiler-generated strings. 181 | */ 182 | _isEnumClassInit(classFile, method) { 183 | if ((classFile.access_flags & Modifier.ENUM) === 0) return false; 184 | 185 | const { name, descriptor } = extractMethodInfoConstants(method, classFile.constant_pool); 186 | return name === "" && descriptor === "()V"; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/StringWriter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import { stringToUtf8ByteArray } from "utf8-string-bytes"; 20 | import { JavaClassFileReader, JavaClassFileWriter } from "java-class-tools"; 21 | 22 | export default class StringWriter { 23 | /** 24 | * (Re)write the strings to a jar file 25 | * 26 | * @param {JSZip} jar - The jar file 27 | * @param {{ fileName: string, value: string, constantIndex: Number }[]} strings - The strings 28 | * @returns {Promise} - A promise of the compressed jar file 29 | */ 30 | static write(jar, strings) { 31 | const classWriter = new JavaClassFileWriter(); 32 | const classReader = new JavaClassFileReader(); 33 | 34 | // We use this to cache the class files we already read. 35 | const classFileMap = new Map(); 36 | const filePromises = []; 37 | 38 | for (const { constantIndex, changed, fileName, value } of strings) { 39 | if (!changed) continue; 40 | 41 | let classFilePromise; 42 | 43 | if (classFileMap.has(fileName)) { 44 | classFilePromise = classFileMap.get(fileName); 45 | } else { 46 | classFilePromise = jar 47 | .file(fileName) 48 | .async("arraybuffer") 49 | .then(buf => classReader.read(buf)); 50 | classFileMap.set(fileName, classFilePromise); 51 | } 52 | 53 | // After we read the class from the jar file 54 | // we rewrite the string to it 55 | classFilePromise.then(classFile => { 56 | // Convert the string to bytes 57 | const stringBytes = stringToUtf8ByteArray(value); 58 | const utf8Entry = 59 | classFile.constant_pool[ 60 | classFile.constant_pool[constantIndex].string_index 61 | ]; 62 | 63 | // Replace the bytes in the constant_pool 64 | utf8Entry.length = stringBytes.length; 65 | utf8Entry.bytes = stringBytes; 66 | 67 | // Write the class file back to the jar 68 | jar.file(fileName, classWriter.write(classFile).buffer); 69 | }); 70 | 71 | filePromises.push(classFilePromise); 72 | } 73 | 74 | // We wait all ZipObject#async's promises then we re-compress the jar file 75 | return Promise.all(filePromises).then(() => 76 | jar.generateAsync({ type: "blob", compression: "DEFLATE" }) 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Button/Button.css: -------------------------------------------------------------------------------- 1 | 2 | .default-button { 3 | color: white; 4 | font-size: 1em; 5 | cursor: pointer; 6 | 7 | border: 1px solid rgb(69, 69, 245); 8 | background: rgb(69, 69, 245); 9 | border-radius: 4px; 10 | } 11 | 12 | .default-button.btn-large { 13 | padding: 10px 35px; 14 | } 15 | 16 | .default-button:disabled { 17 | opacity: .5; 18 | } 19 | 20 | .default-button:focus, .default-button:hover:not(:disabled) { 21 | outline: none; 22 | box-shadow: 0 0 0 3px rgba(160, 160, 160, 0.397); 23 | } 24 | 25 | .default-button:active { 26 | background: rgba(69, 69, 245, .9); 27 | } -------------------------------------------------------------------------------- /src/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React from "react"; 20 | 21 | import "./Button.css"; 22 | 23 | const Button = ({ children, ...props }) => ( 24 | 27 | ); 28 | 29 | export default Button; 30 | -------------------------------------------------------------------------------- /src/components/FileSelector/FileSelector.css: -------------------------------------------------------------------------------- 1 | .file-selector .drag-area { 2 | margin: auto; 3 | 4 | width: 50%; 5 | height: 200px; 6 | 7 | background: white; 8 | box-shadow: 0 1px 6px 0 rgba(13, 13, 14, 0.1); 9 | 10 | text-align: center; 11 | padding: 70px; 12 | cursor: pointer; 13 | 14 | user-select: none; 15 | 16 | transition: box-shadow 0.3s ease; 17 | } 18 | .file-selector .drag-area:hover { 19 | box-shadow: 0 1px 6px 0 rgba(13, 13, 14, 0.3); 20 | } 21 | .file-selector .drag-area:focus { 22 | box-shadow: 0 1px 6px 0 rgba(13, 13, 14, 0.4); 23 | outline: none; 24 | } 25 | .file-selector .drag-area p { 26 | padding-top: 10px; 27 | color: rgba(0, 0, 0, 0.8); 28 | } 29 | .file-selector p.msg-danger { 30 | color: rgb(245, 78, 78); 31 | } 32 | .file-selector input[type=file] { 33 | display: none; 34 | } -------------------------------------------------------------------------------- /src/components/FileSelector/FileSelector.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | import Dropzone from "react-dropzone"; 21 | import { translate } from "../../i18n/i18n"; 22 | import UploadSvg from "../../icons/upload"; 23 | 24 | import "./FileSelector.css"; 25 | 26 | export default class FileSelector extends Component { 27 | state = { 28 | status: "SELECT", 29 | }; 30 | dropzoneRef = React.createRef(); 31 | 32 | renderDropzone = ({ acceptedFiles, rejectedFiles }) => { 33 | let { status } = this.state; 34 | 35 | if (rejectedFiles.length) { 36 | status = "NOT_A_JAR_FILE"; 37 | } else if (acceptedFiles.length) { 38 | const [file] = acceptedFiles; 39 | 40 | status = "LOADING_FILE"; 41 | 42 | // We only care about the catch because we want to show an 43 | // error to the user if we can't load the file. 44 | this.props.onSelected(file).catch(error => { 45 | this.setState({ status: "FAILED_TO_LOAD", error }); 46 | }); 47 | 48 | // Clear accepted files 49 | acceptedFiles.length = 0; 50 | } 51 | 52 | return ( 53 |
54 | 55 | {this.renderStatusMessage(status)} 56 |
57 | ); 58 | }; 59 | 60 | renderStatusMessage = status => { 61 | switch (status) { 62 | case "LOADING_FILE": 63 | return

{translate("file_selector.loading")}

; 64 | 65 | case "NOT_A_JAR_FILE": 66 | return ( 67 |

68 | {translate("file_selector.not_a_jar_file")} 69 |

70 | ); 71 | 72 | case "FAILED_TO_LOAD": 73 | return ( 74 | 75 |

76 | {translate("file_selector.failed_to_load")} 77 |

78 | {this.state.error && ( 79 |

{this.state.error.message}

80 | )} 81 |
82 | ); 83 | 84 | case "SELECT": 85 | return

{translate("file_selector.select")}

; 86 | 87 | // Should never reach here 88 | default: 89 | return

{status}

; 90 | } 91 | }; 92 | 93 | onKeyPress = (e) => { 94 | if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { 95 | // Kinda hack but it works. 96 | // TODO: Maybe update react-dropzone to use it's new api? 97 | this.dropzoneRef.current.onClick(e); 98 | } 99 | }; 100 | 101 | render() { 102 | const dropzoneProps = { 103 | // Remove styles from Dropzone 104 | disabledStyle: {}, 105 | activeStyle: {}, 106 | acceptStyle: {}, 107 | rejectStyle: {}, 108 | style: {}, 109 | disablePreview: true, 110 | multiple: false, 111 | accept: ".jar", 112 | className: "drag-area", 113 | tabIndex: 1, 114 | onKeyPress: this.onKeyPress 115 | }; 116 | 117 | return ( 118 |
119 | {this.renderDropzone} 120 |
121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/SettingsPanel/CheckboxOption.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | 21 | export default class CheckboxOption extends Component { 22 | onChange = ({ target }) => { 23 | this.props.persistTo[this.props.persistKey] = target.checked; 24 | }; 25 | 26 | render() { 27 | return ( 28 |
29 | {this.props.children} 30 | 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/SettingsPanel/InputOption.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | 21 | export default class InputOption extends Component { 22 | onChange = ({ target }) => { 23 | this.props.persistTo[this.props.persistKey] = target.value; 24 | }; 25 | 26 | render() { 27 | const { label, persistTo, persistKey, ...rest } = this.props; 28 | 29 | return ( 30 |
31 | {label} 32 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/SettingsPanel/SelectorOption.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | 21 | export default class SelectorOption extends Component { 22 | onChange = ({ target }) => { 23 | this.props.persistTo[this.props.persistKey] = 24 | target.options[target.selectedIndex].value; 25 | }; 26 | 27 | render() { 28 | const { label, options, defaultValue } = this.props; 29 | 30 | return ( 31 |
32 | {label} 33 | 36 |
37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/SettingsPanel/SettingsPanel.css: -------------------------------------------------------------------------------- 1 | 2 | .settings-container .sidebar.hidden { 3 | transform: translateX(100%); 4 | } 5 | 6 | .settings-container .sidebar.hidden * { 7 | visibility: hidden; 8 | transition: visibility 0.4s 9 | } 10 | 11 | .settings-container .sidebar .actions .done-btn { 12 | position: fixed; 13 | bottom: 14px; 14 | right: 40px; 15 | } 16 | 17 | .settings-container .sidebar .done-btn:hover { 18 | box-shadow: 0 0 0 3px rgba(121, 119, 119, 0.397); 19 | } 20 | 21 | .settings-container .sidebar { 22 | top: 0px; 23 | right: 0px; 24 | position: fixed; 25 | height: 100%; 26 | background: white; 27 | border-left: 1px solid #D7D7DB; 28 | box-shadow: 0 1px 6px 0 rgba(13, 13, 14, 0.1); 29 | 30 | width: 400px; 31 | 32 | transition: 0.3s cubic-bezier(0, 0, 0, 1); 33 | transition-property: transform; 34 | 35 | z-index: 9000; 36 | } 37 | 38 | @media (max-width: 450px) { 39 | .settings-container .sidebar { 40 | width: 100%; 41 | } 42 | } 43 | 44 | .settings-container .sidebar .settings-wrapper { 45 | margin: 40px; 46 | 47 | visibility: visible; 48 | } 49 | 50 | .settings-container .toggle-icon { 51 | padding: 4px; 52 | right: 20px; 53 | display: flex; 54 | position: absolute; 55 | user-select: none; 56 | cursor: pointer; 57 | z-index: 10000; 58 | } 59 | 60 | body.using-mouse .settings-container .toggle-icon:focus { 61 | outline: none; 62 | } 63 | 64 | .settings-container .toggle-icon:hover, 65 | body:not(.using-mouse) .settings-container .toggle-icon:focus { 66 | outline: none; 67 | background: rgba(215, 215, 219, 0.6); 68 | border-radius: 3px; 69 | } 70 | 71 | .settings-wrapper section { 72 | padding-bottom: 5px; 73 | } 74 | 75 | .settings-container label { 76 | user-select: none; 77 | } 78 | 79 | .settings-container label { 80 | user-select: none; 81 | } 82 | 83 | 84 | .option { 85 | display: flex; 86 | justify-content: flex-end 87 | } 88 | 89 | .option .option-right { 90 | margin-right: auto; 91 | } 92 | 93 | .option select { 94 | display: flex; 95 | } -------------------------------------------------------------------------------- /src/components/SettingsPanel/SettingsPanel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | 21 | import settings from "../../settings"; 22 | import { translate, languages, getCurrentLanguage } from "../../i18n/i18n"; 23 | 24 | import SettingsOpenIcon from "../../icons/settings-open"; 25 | import SettingsCloseIcon from "../../icons/settings-close"; 26 | import CheckboxOption from "./CheckboxOption"; 27 | import SelectorOption from "./SelectorOption"; 28 | import InputOption from "./InputOption"; 29 | import { Button } from "../"; 30 | 31 | import "./SettingsPanel.css"; 32 | 33 | const HideEmptyStringsOption = () => ( 34 | 35 | {translate("settings.hide_empty_strings")} 36 | 37 | ); 38 | 39 | const SortByContextOption = () => ( 40 | 41 | {translate("settings.sort_by_context.desc")} 42 | 43 | {translate("settings.sort_by_context.order_desc")} 44 |
    45 |
  • {translate("settings.sort_by_context.order_item.send_message")}
  • 46 |
  • 47 | {translate("settings.sort_by_context.order_item.item_display_name")} 48 |
  • 49 |
50 |
51 |
52 | ); 53 | 54 | const LanguageSelectorOption = () => ( 55 | 62 | ); 63 | 64 | const DebounceRateOption = () => ( 65 | 75 | ); 76 | 77 | export default class SettingsPanel extends Component { 78 | state = { hidden: true }; 79 | 80 | onSave = () => { 81 | settings.save(); 82 | this.hide(); 83 | }; 84 | 85 | toggle = () => { 86 | if (this.state.hidden) { 87 | this.show(); 88 | } else { 89 | this.hide(); 90 | } 91 | }; 92 | 93 | componentWillUnmount() { 94 | this._removeClickOutsideListener(); 95 | } 96 | 97 | show = () => { 98 | this.setState({ hidden: false }); 99 | this._addClickOutsideListener(); 100 | }; 101 | 102 | hide = () => { 103 | this.setState({ hidden: true }); 104 | this._removeClickOutsideListener(); 105 | }; 106 | 107 | onKeyPress = (e) => { 108 | if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { 109 | this.toggle(); 110 | } 111 | }; 112 | 113 | render() { 114 | const { hidden } = this.state; 115 | 116 | const settingsToggleIcon = hidden ? ( 117 | 118 | ) : ( 119 | 120 | ); 121 | 122 | return ( 123 |
124 | 125 | {settingsToggleIcon} 126 | 127 | 128 |
129 |
130 |

{translate("settings.title")}

131 | 132 |
{translate("settings.general")}
133 | 134 | 135 |
136 | 137 |
138 | 139 |
140 | 141 |
{translate("settings.bukkit_specific")}
142 | 143 |
144 | 145 |
146 | 149 |
150 |
151 |
152 | ); 153 | } 154 | 155 | _onClickOutside = e => { 156 | const path = e.path || (e.composedPath && e.composedPath()); 157 | 158 | // If no path, just don't do anything... (Should not happen) 159 | if (!path) return; 160 | 161 | const clickedInside = path.find(e => e.className === "settings-container"); 162 | if (!clickedInside) this.hide(); 163 | }; 164 | 165 | _removeClickOutsideListener = () => { 166 | window.removeEventListener("click", this._onClickOutside); 167 | }; 168 | 169 | _addClickOutsideListener = () => { 170 | window.addEventListener("click", this._onClickOutside); 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /src/components/StringList/StringEntry.css: -------------------------------------------------------------------------------- 1 | .string-entry { 2 | display: flex; 3 | margin: 0 50px 1px 50px; 4 | } 5 | 6 | @media (min-width: 300px) and (max-width: 800px) { 7 | .string-entry { 8 | margin: 0 10px 1px 10px; 9 | } 10 | } 11 | 12 | /* Reset input style */ 13 | input.string-input { 14 | border: none; 15 | background-image: none; 16 | background-color: transparent; 17 | -webkit-box-shadow: none; 18 | -moz-box-shadow: none; 19 | box-shadow: none; 20 | padding: 0px; 21 | } 22 | 23 | .string-entry .string-input { 24 | cursor: text; 25 | } 26 | 27 | .string-entry .string-input { 28 | flex: 1; 29 | 30 | padding: 2px; 31 | font-size: 1em; 32 | border: 1px solid rgba(0, 0, 0, .3); 33 | background: white; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | overflow: hidden; 37 | } 38 | .string-entry .string-input:focus { 39 | outline: none; 40 | border: 1px solid rgba(0, 0, 0, .7); 41 | } 42 | 43 | .string-entry .string-info { 44 | position: relative; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | width: 25px; 49 | border: 1px solid rgba(0, 0, 0, .3); 50 | margin-left: 2px; 51 | background: rgba(13, 13, 14, 0.07); 52 | } 53 | 54 | body.using-mouse .string-entry .string-info:focus { 55 | outline: none; 56 | } 57 | 58 | body:not(.using-mouse) .string-entry .string-info:focus { 59 | outline: none; 60 | border: 1px solid rgba(0, 0, 0, .8); 61 | background: rgba(13, 13, 14, 0.10); 62 | } 63 | 64 | .string-entry .string-info-tooltip { 65 | position: absolute; 66 | background: white; 67 | border-left: 1px solid rgb(206, 206, 207); 68 | box-shadow: 0 2px 6px 0 rgba(13, 13, 14, 0.2); 69 | padding: 10px; 70 | z-index: 1000; 71 | right: 0; 72 | margin-right: 26px; 73 | min-width: 350px; 74 | 75 | visibility: hidden; 76 | opacity: 0; 77 | 78 | transition: opacity 0.15s, visibility 0.2s; 79 | } 80 | .string-entry .string-info-tooltip span { 81 | width: max-content; 82 | overflow-wrap: break-word;; 83 | max-width: 80vw; 84 | } 85 | 86 | .string-entry .string-info-tooltip:hover, 87 | .string-entry .string-info:hover .string-info-tooltip, 88 | body:not(.using-mouse) .string-entry .string-info:focus-within .string-info-tooltip { 89 | display: block; 90 | visibility: visible; 91 | opacity: 1; 92 | } 93 | 94 | .string-entry .string-info-tooltip span { 95 | display: block; 96 | } 97 | 98 | .view-class { 99 | margin-top: 2px; 100 | } 101 | 102 | .view-class > button { 103 | float: right; 104 | padding: 3px 5px; 105 | } -------------------------------------------------------------------------------- /src/components/StringList/StringEntry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | import React, { Component } from "react"; 19 | import HighlightWords from "react-highlight-words"; 20 | 21 | import { prettyMethodInfo } from "../../util/util.js"; 22 | 23 | import InfoIcon from "../../icons/info"; 24 | 25 | import "./StringEntry.css"; 26 | import { translate } from "../../i18n/i18n"; 27 | 28 | export default class StringEntry extends Component { 29 | state = { focused: false }; 30 | 31 | onDivClick = ({ target }) => { 32 | this.setState({ focused: true }); 33 | }; 34 | 35 | onInput = ({ target }) => { 36 | const { string } = this.props; 37 | 38 | // If highlightWords is not undefined it means that the string object was cloned 39 | // so we need to update it's value here. 40 | if (string.highlightWords) { 41 | string.value = target.value; 42 | } 43 | 44 | this.props.onChanged(target.value, string.id); 45 | }; 46 | 47 | onFakeInputFocus = (e) => { 48 | this.setState({ focused: true }); 49 | }; 50 | 51 | componentDidUpdate() { 52 | if (this.state.focused) { 53 | const { input } = this; 54 | 55 | input.focus(); 56 | 57 | // Place the caret at end 58 | setImmediate(() => { 59 | input.selectionStart = input.selectionEnd = input.value.length; 60 | }); 61 | } 62 | } 63 | 64 | onInputBlur = () => { 65 | this.setState({ focused: false }); 66 | }; 67 | 68 | handleViewClass = () => { 69 | const { string, handleViewClass } = this.props; 70 | if (typeof handleViewClass === "function") { 71 | handleViewClass(string); 72 | } 73 | }; 74 | 75 | render() { 76 | const { string } = this.props; 77 | const { value, highlightWords } = string; 78 | 79 | let element; 80 | 81 | if (!highlightWords || this.state.focused) { 82 | element = ( 83 | { 86 | this.input = input; 87 | }} 88 | onBlur={this.onInputBlur} 89 | type="text" 90 | className="string-input" 91 | defaultValue={value} 92 | /> 93 | ); 94 | } else { 95 | element = ( 96 |
97 | 103 |
104 | ); 105 | } 106 | 107 | return ( 108 |
109 | {element} 110 |
111 | 112 | 113 |
114 |
115 | ); 116 | } 117 | } 118 | 119 | const StringInfo = ({ string: { location, context }, handleViewClass }) => ( 120 |
121 | 122 | {translate("app.string_info.class", { 123 | className: {location.className}, 124 | })} 125 | 126 | 127 | {translate("app.string_info.method", { 128 | method: {prettyMethodInfo(location.method)}, 129 | })} 130 | 131 | {location.lineNumber && ( 132 | 133 | {translate("app.string_info.line", { 134 | lineNumber: {location.lineNumber}, 135 | })} 136 | 137 | )} 138 | {context && ( 139 | 140 | {translate("app.string_info.context", { 141 | context: {context}, 142 | })} 143 | 144 | )} 145 |
146 | 147 |
148 |
149 | ); 150 | -------------------------------------------------------------------------------- /src/components/StringList/StringList.css: -------------------------------------------------------------------------------- 1 | .strings-container { 2 | margin-top: 10px; 3 | margin-bottom: 50px; 4 | } 5 | 6 | .string-list { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | .load-more-btn-container { 13 | margin-top: 20px; 14 | display: flex; 15 | justify-content: center; 16 | } -------------------------------------------------------------------------------- /src/components/StringList/StringList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React, { Component } from "react"; 20 | 21 | import StringEntry from "./StringEntry"; 22 | import "./StringList.css"; 23 | import { translate } from "../../i18n/i18n"; 24 | 25 | import { Button } from "../"; 26 | 27 | export default class StringList extends Component { 28 | static LOAD_MORE_AMOUNT = 50; 29 | 30 | state = { limitToRender: StringList.LOAD_MORE_AMOUNT }; 31 | 32 | loadMore = () => { 33 | const { props, state } = this; 34 | 35 | if (state.limitToRender >= props.strings.length) { 36 | return; 37 | } 38 | 39 | this.setState({ 40 | limitToRender: state.limitToRender + StringList.LOAD_MORE_AMOUNT, 41 | }); 42 | }; 43 | 44 | render() { 45 | const { strings } = this.props; 46 | const { limitToRender } = this.state; 47 | 48 | const remaining = strings.length - limitToRender; 49 | 50 | return ( 51 |
52 |
53 | {strings.slice(0, limitToRender).map((string, index) => { 54 | return ( 55 | 62 | ); 63 | })} 64 |
65 | {remaining > 0 && ( 66 |
67 | 70 |
71 | )} 72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import StringList from "./StringList/StringList"; 20 | import FileSelector from "./FileSelector/FileSelector"; 21 | import SettingsPanel from "./SettingsPanel/SettingsPanel"; 22 | import Button from "./Button/Button"; 23 | 24 | export { Button, FileSelector, SettingsPanel, StringList }; 25 | -------------------------------------------------------------------------------- /src/i18n/i18n.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import { flatten } from "flat"; 20 | import { compile } from "./string-template"; 21 | import settings from "../settings"; 22 | 23 | // Get default language based on navigator.language 24 | const getDefaultLanguage = () => { 25 | if (navigator.language.startsWith("pt-")) { 26 | return "pt-BR"; 27 | } 28 | return "en-US"; 29 | }; 30 | 31 | export const getCurrentLanguage = () => 32 | settings.language || getDefaultLanguage(); 33 | 34 | export const languages = { 35 | "en-US": require("./langs/en.json"), 36 | "pt-BR": require("./langs/pt-BR.json"), 37 | }; 38 | 39 | /** 40 | * @param {String} key 41 | * @param {*} props 42 | * @returns {String | Any[]} 43 | */ 44 | export const translate = (key, props) => { 45 | const lang = 46 | languages[getCurrentLanguage()] || languages[getDefaultLanguage()]; 47 | const value = lang[key]; 48 | 49 | if (value === undefined) { 50 | console.error( 51 | `i18n: missing key "${key}" in language "${getCurrentLanguage()}"` 52 | ); 53 | return `i18n: missing key ${key}`; 54 | } 55 | 56 | // It is a compiled string template 57 | if (typeof value === "function") { 58 | return value(props); 59 | } 60 | 61 | return value; 62 | }; 63 | 64 | // Flatten lang objects 65 | for (const key in languages) { 66 | languages[key] = flatten(languages[key]); 67 | } 68 | 69 | // Compile string templates 70 | for (const lang in languages) { 71 | for (const key in languages[lang]) { 72 | languages[lang][key] = compile(languages[lang][key]); 73 | } 74 | } 75 | 76 | // Warns if a key exists in a language but not in another 77 | if (process.env.NODE_ENV === "development") { 78 | const visitedKeys = {}; 79 | const allLangs = []; 80 | 81 | for (const lang in languages) { 82 | for (const key in languages[lang]) { 83 | if (!visitedKeys[key]) { 84 | visitedKeys[key] = new Set(); 85 | } 86 | 87 | visitedKeys[key].add(lang); 88 | } 89 | allLangs.push(lang); 90 | } 91 | 92 | for (const key in visitedKeys) { 93 | const langsVisited = visitedKeys[key]; 94 | const notVisited = allLangs.filter(lang => !langsVisited.has(lang)); 95 | 96 | if (langsVisited.size !== allLangs.length) { 97 | console.warn( 98 | `I18n: the key "${key}" exists in: [${Array.from(langsVisited).join( 99 | ", " 100 | )}], but not in: [${notVisited}]` 101 | ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/i18n/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_selector": { 3 | "select": "Click to select a JAR file or drag it here.", 4 | "failed_to_load": "Failed to load the selected file.", 5 | "not_a_jar_file": "The selected file is not a valid jar file.", 6 | "loading": "Loading file..." 7 | }, 8 | "settings": { 9 | "select_language": "Select language", 10 | "hide_empty_strings": "Hide empty strings.", 11 | "title": "Settings", 12 | "apply": "Apply", 13 | "sort_by_context": { 14 | "desc": "Order by relevance", 15 | "order_desc": "Order of relevance:", 16 | "order_item": { 17 | "send_message": "SendMessage - The messages sent to a player.", 18 | "item_display_name": "ItemDisplayName - The display name of an item." 19 | } 20 | }, 21 | "general": "General", 22 | "bukkit_specific": "Bukkit-specific", 23 | "debounce_rate": "Search refresh rate (ms)" 24 | }, 25 | "app": { 26 | "collecting_strings": "Collecting strings... {{progress}}% ({{numDone}}/{{numClasses}} classes)", 27 | "created_by": "Created with a lot of {{coffee}} by {{link}}.", 28 | "search": "Search", 29 | "save": "Save", 30 | "strings_info": "{{found}} strings found | {{after_filter}} after filter ({{took}}ms)", 31 | "string_info": { 32 | "class": "Class: {{className}}", 33 | "method": "Method: {{method}}", 34 | "line": "Line Number: {{lineNumber}}", 35 | "context": "Context: {{context}}" 36 | }, 37 | "load_more": "Load more", 38 | "view_class_file": "Open class file on JDec" 39 | } 40 | } -------------------------------------------------------------------------------- /src/i18n/langs/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_selector": { 3 | "select": "Clique para selecionar um arquivo JAR ou arraste-o até aqui.", 4 | "failed_to_load": "Não foi possível carregar o arquivo selecionado.", 5 | "not_a_jar_file": "O arquivo selecionado não é um arquivo jar válido.", 6 | "loading": "Carregando arquivo..." 7 | }, 8 | "settings": { 9 | "select_language": "Selecionar idioma", 10 | "hide_empty_strings": "Ocultar strings vazias.", 11 | "title": "Configurações", 12 | "apply": "Aplicar", 13 | "sort_by_context": { 14 | "desc": "Ordenar por relevância", 15 | "order_desc": "Ordem de relevância:", 16 | "order_item": { 17 | "send_message": "SendMessage - Mensagens enviadas para o jogador.", 18 | "item_display_name": "ItemDisplayName - Nome de exibição de items." 19 | } 20 | }, 21 | "general": "Geral", 22 | "bukkit_specific": "Específico do Bukkit", 23 | "debounce_rate": "Taxa de atualização da pesquisa (ms)" 24 | }, 25 | "app": { 26 | "collecting_strings": "Coletando strings... {{progress}}% ({{numDone}}/{{numClasses}} classes)", 27 | "created_by": "Criado com muito {{coffee}} por {{link}}.", 28 | "search": "Pesquisar", 29 | "save": "Salvar", 30 | "strings_info": "{{found}} strings encontradas | {{after_filter}} após filtro ({{took}}ms)", 31 | "string_info": { 32 | "class": "Classe: {{className}}", 33 | "method": "Método: {{method}}", 34 | "line": "Linha: {{lineNumber}}", 35 | "context": "Contexto: {{context}}" 36 | }, 37 | "load_more": "Carregar mais", 38 | "view_class_file": "Abrir classe no JDec" 39 | } 40 | } -------------------------------------------------------------------------------- /src/i18n/string-template.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | const compile = text => { 20 | const tokens = []; 21 | const vars = []; 22 | let pos = -1; 23 | let lastSubstring = 0; 24 | 25 | while (pos++ < text.length) { 26 | // Check start var 27 | if (isOpenVarAt(text, pos)) { 28 | let varNameStart = pos + 2; // Skip {{ 29 | let varNameEnd = varNameStart; 30 | 31 | for (let auxPos = pos + 2; auxPos < text.length; auxPos++) { 32 | if (isOpenVarAt(text, auxPos)) { 33 | console.warn( 34 | `possible unterminated varname at '${text.substring( 35 | 0, 36 | varNameStart 37 | )}'` 38 | ); 39 | break; 40 | } 41 | 42 | // Check var end 43 | if (isCloseVarAt(text, auxPos)) { 44 | varNameEnd = auxPos; 45 | break; 46 | } 47 | } 48 | 49 | if (varNameEnd - varNameStart > 0) { 50 | const varName = text.substring(varNameStart, varNameEnd); 51 | 52 | // include the text before the variable 53 | tokens.push(text.substring(lastSubstring, varNameStart - 2)); 54 | // include the variable 55 | tokens.push({ varName }); 56 | 57 | lastSubstring = varNameEnd + 2; 58 | 59 | vars.push(varName); 60 | } 61 | } 62 | } 63 | // cut the rest 64 | tokens.push(text.substring(lastSubstring, text.length)); 65 | 66 | // If there's no vars, we don't need to create a function 67 | if (vars.length === 0) { 68 | return text; 69 | } 70 | 71 | /** 72 | * Convert tokens to an array js values 73 | * E.g 74 | * ["Hello, ", {varName: "name"}] ==> ["Hello, ", vars["name"]] 75 | */ 76 | const arrayValues = tokens 77 | .map(token => { 78 | if (token.varName) { 79 | const { varName } = token; 80 | // (vars["varName"] || "{{varName}}") 81 | return `(vars["${varName}"] == undefined ? "{{${varName}}}" : vars["${varName}"])`; 82 | } 83 | // string 84 | return `"${token}"`; 85 | }) 86 | .join(", "); 87 | 88 | /* eslint-disable no-new-func */ 89 | return new Function( 90 | "vars", 91 | ` 92 | vars = vars || {}; 93 | return [${arrayValues}]; 94 | ` 95 | ); 96 | }; 97 | 98 | const isOpenVarAt = (text, pos) => 99 | text.charAt(pos) === "{" && text.charAt(pos + 1) === "{"; 100 | 101 | const isCloseVarAt = (text, pos) => 102 | text.charAt(pos) === "}" && text.charAt(pos + 1) === "}"; 103 | 104 | export { compile }; 105 | -------------------------------------------------------------------------------- /src/icons/coffee.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 14 | Coffee 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /src/icons/gear.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 14 | 15 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /src/icons/info.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/icons/settings-close.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 5 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/icons/settings-open.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 5 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/icons/upload.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import React from "react"; 20 | import { render } from "react-dom"; 21 | import App from "./App"; 22 | import registerServiceWorker from "./util/registerServiceWorker"; 23 | 24 | render(, document.getElementById("root")); 25 | registerServiceWorker(); 26 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | const DEFAULT_SETTINGS = { 20 | hideEmptyStrings: true, 21 | sortByContext: true, 22 | debounceRate: 30, 23 | }; 24 | 25 | // Load from localStorage 26 | function load() { 27 | // Safe load from localStorage. 28 | let settings; 29 | try { 30 | settings = JSON.parse(localStorage.getItem("jse-settings")); 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | 35 | // We only copy defaults if settings was sucessfully loaded, otherwise 36 | // it will already return the DEFAULT_SETTINGS 37 | if (settings) { 38 | copyDefaults(DEFAULT_SETTINGS, settings); 39 | } 40 | 41 | return settings || DEFAULT_SETTINGS; 42 | } 43 | 44 | function copyDefaults(from, to) { 45 | for (const key in from) { 46 | if (from[key] !== null && typeof from[key] === "object") { 47 | copyDefaults(from[key], to[key] || (to[key] = {})); 48 | continue; 49 | } 50 | if (!to.hasOwnProperty(key) || to[key] === null) { 51 | to[key] = from[key]; 52 | } 53 | } 54 | } 55 | 56 | // Save to localStorage 57 | function save() { 58 | observers.forEach(callback => callback(oldSettings)); 59 | oldSettings = { ...settings }; 60 | localStorage.setItem("jse-settings", JSON.stringify(this)); 61 | } 62 | 63 | // Simple way to observe when settings are saved 64 | function observe(listener) { 65 | observers.push(listener); 66 | } 67 | 68 | const observers = []; 69 | const settings = load(); 70 | 71 | settings.save = save.bind(settings); 72 | settings.observe = observe.bind(settings); 73 | 74 | // Used in observers 75 | let oldSettings = { ...settings }; 76 | 77 | export default settings; 78 | -------------------------------------------------------------------------------- /src/util/descriptor-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | /** 20 | * Parse a field descriptor 21 | * 22 | * @see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.3.2 23 | * @param {String} descriptor - The descriptor 24 | * @return {{ type: String, value: String }} - The parsed descriptor 25 | */ 26 | export function parseFieldType(descriptor) { 27 | let [char] = descriptor; 28 | let charIndex = 1; 29 | 30 | if (char === "L") { 31 | const classNameEnd = descriptor.indexOf(";", charIndex); 32 | return { 33 | type: "class", 34 | value: descriptor.substring(charIndex, classNameEnd), 35 | }; 36 | } 37 | 38 | // ArrayType 39 | if (char === "[") { 40 | let dimensions = 1; 41 | 42 | while (descriptor[charIndex++] === "[") { 43 | dimensions++; 44 | } 45 | 46 | const arrayType = parseFieldType(descriptor.substring(charIndex - 1)); 47 | 48 | return { 49 | type: "array", 50 | dimensions, 51 | arrayType, 52 | value: arrayType.value + "[]".repeat(dimensions), 53 | }; 54 | } 55 | 56 | // BaseType 57 | let value; 58 | 59 | switch (char) { 60 | case "B": 61 | value = "byte"; 62 | break; 63 | case "C": 64 | value = "char"; 65 | break; 66 | case "D": 67 | value = "double"; 68 | break; 69 | case "F": 70 | value = "float"; 71 | break; 72 | case "I": 73 | value = "int"; 74 | break; 75 | case "J": 76 | value = "long"; 77 | break; 78 | case "S": 79 | value = "short"; 80 | break; 81 | case "Z": 82 | value = "boolean"; 83 | break; 84 | 85 | default: 86 | throw new Error( 87 | `Invalid descriptor. ch=${char}, descriptor=${descriptor}` 88 | ); 89 | } 90 | 91 | return { type: "primitive", value }; 92 | } 93 | 94 | /** 95 | * Return the size of a fieldType 96 | * @param {*} fieldType 97 | * @see parseFieldType(descriptor) 98 | * @return {Number} 99 | */ 100 | function sizeOfType(fieldType) { 101 | switch (fieldType.type) { 102 | case "primitive": 103 | return 1; 104 | case "class": 105 | return 2 + fieldType.value.length; 106 | case "array": 107 | return 1 * fieldType.dimensions + sizeOfType(fieldType.arrayType); 108 | default: 109 | throw new Error(`Unexpected type: ${fieldType}`); 110 | } 111 | } 112 | 113 | /** 114 | * Parse a method descriptor 115 | * 116 | * @see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.3.3 117 | * @param {String} descriptor 118 | * @returns {{ parameters: { type: String, value: String }[], returnType: { type: String, value: String } }} 119 | */ 120 | export function parseMethodDescriptor(descriptor) { 121 | const parameters = []; 122 | let charIndex = 1; // Ignore ( 123 | let char = descriptor[charIndex]; 124 | 125 | while (char !== undefined && char !== ")") { 126 | const fieldType = parseFieldType(descriptor.substring(charIndex)); 127 | charIndex += sizeOfType(fieldType); 128 | char = descriptor[charIndex]; 129 | parameters.push(fieldType); 130 | } 131 | 132 | const returnType = descriptor.substring(++charIndex); 133 | return { 134 | parameters, 135 | returnType: 136 | returnType === "V" 137 | ? { type: "void", value: "void" } 138 | : parseFieldType(returnType), 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /src/util/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === "installed") { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log("New content is available; please refresh."); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log("Content is cached for offline use."); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error("Error during service worker registration:", error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get("content-type").indexOf("javascript") === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | "No internet connection found. App is running in offline mode." 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ("serviceWorker" in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/util/string-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping 20 | export function escapeRegExp(string) { 21 | return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 22 | } -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2020 leonardosnt (leonrdsnt@gmail.com) 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License along 15 | * with this program; if not, write to the Free Software Foundation, Inc., 16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | import { utf8ByteArrayToString } from "utf8-string-bytes"; 20 | import { parseMethodDescriptor } from "./descriptor-parser"; 21 | import { Opcode } from "java-class-tools"; 22 | 23 | /** 24 | * Get an attribute by its name. 25 | * 26 | * @param {ClassFile} classFile - The class file 27 | * @param {any} source - Object you want get attribute from 28 | * @param {string} attributeName - Attribute name 29 | * @returns {any} - The attribute 30 | */ 31 | export function getAttribute(classFile, source, attributeName) { 32 | const { attributes } = source; 33 | 34 | if (attributes === undefined) { 35 | throw new Error("target does not have attributes"); 36 | } 37 | 38 | return attributes.find(attr => { 39 | const nameBytes = classFile.constant_pool[attr.attribute_name_index].bytes; 40 | return utf8ByteArrayToString(nameBytes) === attributeName; 41 | }); 42 | } 43 | 44 | export function getInstructionLineNumber(classFile, method, instruction) { 45 | let lineNumber = undefined; 46 | 47 | const codeAttr = getAttribute(classFile, method, "Code"); 48 | const lineNumberTable = getAttribute(classFile, codeAttr, "LineNumberTable"); 49 | const { bytecodeOffset } = instruction; 50 | 51 | if (lineNumberTable !== undefined) { 52 | for (let i = 0; i < lineNumberTable.line_number_table_length - 1; i++) { 53 | const { start_pc, line_number } = lineNumberTable.line_number_table[i]; 54 | const end_pc = lineNumberTable.line_number_table[i + 1].start_pc; 55 | 56 | if (bytecodeOffset >= start_pc && bytecodeOffset <= end_pc) { 57 | lineNumber = line_number; 58 | break; 59 | } 60 | } 61 | } 62 | 63 | return lineNumber; 64 | } 65 | 66 | /** 67 | * @param {{ name: String, descriptor: String}} method 68 | * @return {String} 69 | */ 70 | export function prettyMethodInfo({ name, descriptor }) { 71 | const parsedDescriptor = parseMethodDescriptor(descriptor); 72 | const params = parsedDescriptor.parameters 73 | .map( 74 | p => 75 | p.value.replace( 76 | "java/lang/", 77 | '' 78 | ) /* remove java.lang package if present */ 79 | ) 80 | .join(", "); 81 | 82 | return `${parsedDescriptor.returnType.value} ${name}(${params})`; 83 | } 84 | 85 | export function extractClassName(index, constant_pool) { 86 | return getUtf8String(constant_pool, constant_pool[index].name_index); 87 | } 88 | 89 | export function extractMethodInfoConstants(methodInfoOrIndex, constant_pool) { 90 | const methodInfo = 91 | typeof methodInfoOrIndex === "number" 92 | ? constant_pool[methodInfoOrIndex] 93 | : methodInfoOrIndex; 94 | return { 95 | name: getUtf8String(constant_pool, methodInfo.name_index), 96 | descriptor: getUtf8String(constant_pool, methodInfo.descriptor_index), 97 | }; 98 | } 99 | 100 | export function getUtf8String(constant_pool, index) { 101 | let poolEntry = constant_pool[index]; 102 | 103 | if (poolEntry.tag === 8) { 104 | // CONSTANT_String_info 105 | poolEntry = constant_pool[poolEntry.string_index]; 106 | } else if (poolEntry.tag !== 1) { 107 | // CONSTANT_Utf8_info 108 | throw new Error("constant_pool[index] does not represent a string."); 109 | } 110 | 111 | return utf8ByteArrayToString(poolEntry.bytes); 112 | } 113 | 114 | /** 115 | * @param {ConstantPoolInfo[]} constantPool 116 | * @param {Instruction[]} instructions 117 | * @param {number} index - Index in instructions 118 | */ 119 | export function getStringContext(constantPool, instructions, index) { 120 | // TODO: Since we only look at the next instruction, 121 | // this won't work with string concatenations. 122 | 123 | const nextInstruction = instructions[index + 1]; 124 | 125 | if (nextInstruction.opcode === Opcode.INVOKEINTERFACE) { 126 | const operands = nextInstruction.operands; 127 | const index = (operands[0] << 8) | operands[1]; 128 | const methodRef = constantPool[index]; 129 | const className = extractClassName(methodRef.class_index, constantPool); 130 | const { name, descriptor } = extractMethodInfoConstants( 131 | methodRef.name_and_type_index, 132 | constantPool 133 | ); 134 | 135 | const fullMethodDesc = `${className}#${name}${descriptor}`; 136 | 137 | switch (fullMethodDesc) { 138 | case "org/bukkit/command/CommandSender#sendMessage(Ljava/lang/String;)V": 139 | case "org/bukkit/entity/Player#sendMessage(Ljava/lang/String;)V": 140 | return "SendMessage"; 141 | 142 | case "org/bukkit/inventory/meta/ItemMeta#setDisplayName(Ljava/lang/String;)V": 143 | return "ItemDisplayName"; 144 | 145 | default: 146 | return undefined; 147 | } 148 | } 149 | }; --------------------------------------------------------------------------------