Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@
float: right;
}

.replace-inputs {
display: flex;
flex-direction: column;
margin-top: 4px;
}
.replace-inputs input {
height: 20px;
margin-bottom: 5px;
}
.replace .button {
float: right;
}

.load-info-box {
margin: auto;

Expand Down
77 changes: 75 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import CoffeIcon from './icons/coffee';
import './App.css';

class App extends Component {
// TODO: merge the context property directly in the state?
static INITIAL_CONTEXT = Object.freeze({
loadedJar: undefined,
selectedFileName: undefined,
Expand Down Expand Up @@ -134,6 +135,7 @@ class App extends Component {
method,
...rest
} = string;

const value = getUtf8String(classFile.constant_pool, constantIndex);
const location = getInstructionLocation(
classFile,
Expand Down Expand Up @@ -198,6 +200,7 @@ class App extends Component {
};

onStringChanged = (newValue, stringId) => {
// TODO: use better (constant) lookup
const string = this.state.context.strings.find(s => s.id === stringId);

if (newValue !== string.value) {
Expand Down Expand Up @@ -250,8 +253,24 @@ class App extends Component {
</div>
);

onStringsReplaced = newStrings => {
this.setState(state =>
update(state, {
context: {
strings: { $set: newStrings },
},
})
);
};

onToggleReplace = () => {
this.setState(({ displayReplace }) => ({
displayReplace: !displayReplace,
}));
};

render() {
const { loadInfo, context } = this.state;
const { loadInfo, context, displayReplace } = this.state;

if (context.loadedJar === undefined) {
return this.renderAppContainer(
Expand Down Expand Up @@ -288,10 +307,23 @@ class App extends Component {
after_filter: filtered.length,
})}
</span>
<Button onClick={this.onSaveFile} className="save-btn">
<Button onClick={this.onSaveFile} style={{ float: 'right' }}>
{translate('app.save')}
</Button>
<Button
onClick={this.onToggleReplace}
style={{ float: 'right', marginRight: '2px' }}
>
Replace
</Button>
</div>

{displayReplace && (
<StringReplace
onReplaced={this.onStringsReplaced}
strings={context.strings}
/>
)}
</div>

<StringList onStringChanged={this.onStringChanged} strings={filtered} />
Expand All @@ -316,6 +348,47 @@ class App extends Component {
};
}

// TODO: add regex support?
class StringReplace extends Component {
inputFromRef = React.createRef();
inputToRef = React.createRef();

replaceStrings = () => {
const { strings } = this.props;
const fromValue = this.inputFromRef.current.value;
const toValue = this.inputToRef.current.value;

if (!fromValue || !toValue) {
return;
}

for (const string of strings) {
if (!stringContains(string.value, fromValue, true)) {
continue;
}
string.value = string.value.replace(fromValue, toValue);
string.changed = true;
}

this.props.onReplaced(strings);
};

render() {
return (
<div className="replace">
Replace
<div className="replace-inputs">
<input ref={this.inputFromRef} type="text" placeholder="From" />
<input ref={this.inputToRef} type="text" placeholder="To" />
</div>
<Button style={{ float: 'right' }} onClick={this.replaceStrings}>
Replace
</Button>
</div>
);
}
}

const Link = props => (
<a
target="_blank"
Expand Down
97 changes: 47 additions & 50 deletions src/components/StringList/StringEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

import React, { Component } from 'react';
import HighlightWords from 'react-highlight-words';

Expand All @@ -26,77 +27,73 @@ import './StringEntry.css';
import { translate } from '../../i18n/i18n';

export default class StringEntry extends Component {
state = { focused: false };
inputRef = React.createRef();

constructor(props) {
super(props);

this.state = {
focused: false,
value: props.string.value,
};
}

onDivClick = ({ target }) => {
onDivClick = () => {
this.setState({ focused: true });
};

onInput = ({ target }) => {
const { string } = this.props;
onChange = ({ target }) => {
this.setState({ value: target.value });
};

// If highlightWords is not undefined it means that the string object was cloned
// so we need to update it's value here.
if (string.highlightWords) {
string.value = target.value;
componentWillReceiveProps(nextProps) {
if (this.state.value !== nextProps.string.value) {
// We don't care if we erase the state.
this.setState({ value: nextProps.string.value });
}

this.props.onChanged(target.value, string.id);
};
}

componentDidUpdate() {
// We use this to focus the input when it changes from the
// higlighted div. If we don't do this, the user will need to
// click twice to focus the input.
if (this.state.focused) {
const { input } = this;

input.focus();

// Place the caret at end
setImmediate(() => {
input.selectionStart = input.selectionEnd = input.value.length;
});
this.inputRef.current.focus();
}
}

onInputBlur = () => {
this.setState({ focused: false });
this.props.onChanged(this.state.value, this.props.string.id);
};

render() {
const { string } = this.props;
const { value, highlightWords } = string;

let element;

if (!highlightWords || this.state.focused) {
element = (
<input
onInput={this.onInput}
ref={input => {
this.input = input;
}}
onBlur={this.onInputBlur}
type="text"
className="string-input"
defaultValue={value}
/>
);
} else {
// TODO: tab select? focus & allow to use enter to open the file selector
element = (
<div onClick={this.onDivClick} className="string-input">
<HighlightWords
highlightClassName={'string-highlight'}
searchWords={highlightWords}
autoEscape={true}
textToHighlight={value}
/>
</div>
);
}

const useFakeHighlightedInput =
string.highlightWords && !this.state.focused;

return (
<div className="string-entry">
{element}
{useFakeHighlightedInput ? (
<div onClick={this.onDivClick} className="string-input">
<HighlightWords
highlightClassName={'string-highlight'}
searchWords={string.highlightWords}
autoEscape={true}
textToHighlight={this.state.value}
/>
</div>
) : (
<input
onChange={this.onChange}
ref={this.inputRef}
onBlur={this.onInputBlur}
type="text"
className="string-input"
value={this.state.value}
/>
)}
<div className="string-info">
<InfoIcon />
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/util/string-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* @param {string} haystack
* @param {string} needle
*/
export function stringContains(haystack, needle) {
export function stringContains(haystack, needle, caseSensitive = false) {
let hay_pos = 0;
let need_pos = 0;
let eq_count = 0;
Expand All @@ -29,15 +29,14 @@ export function stringContains(haystack, needle) {
const hay_cur_char = haystack.charCodeAt(hay_pos++),
needle_cur_char = needle.charCodeAt(need_pos++);

// Ignore case
const case_diff =
hay_cur_char >= 97 && hay_cur_char <= 122
? -32
: hay_cur_char >= 65 && hay_cur_char <= 90 ? 32 : 0;

if (
hay_cur_char === needle_cur_char ||
hay_cur_char + case_diff === needle_cur_char
(!caseSensitive && hay_cur_char + case_diff === needle_cur_char)
) {
eq_count++;
} else {
Expand Down