import {html} from 'htm/preact';
import {Component} from 'preact';
import browser from 'webextension-polyfill';
import Bang, {BangParameters} from '../../bang/bang.js';
type Props = Record;
type State = {
editorBang: BangParameters;
editorError: string | undefined;
bangs: BangParameters[];
};
export class PageMain extends Component {
emptyBang: BangParameters;
constructor(props: Props) {
super(props);
this.emptyBang = {
baseUrl: '',
id: '',
name: '',
searchUrl: '',
};
this.state = {
bangs: [],
editorBang: {...this.emptyBang},
editorError: undefined,
};
}
async componentDidMount() {
const localStorage = await browser.storage.local.get();
const bangs = Object.entries(localStorage)
.filter(([key, _bang]) => key.startsWith('!'))
.map(([_key, bang]) => bang as BangParameters);
this.setState({bangs: this.state.bangs.concat(bangs)});
}
editBang = (event: Event, key: keyof BangParameters) => {
const input = event.target as HTMLInputElement;
this.state.editorBang[key] = input.value;
let editorError;
try {
Bang.validate(this.state.editorBang);
} catch (error: unknown) {
editorError = (error as Error).message;
}
this.setState({
editorBang: this.state.editorBang,
editorError,
});
};
removeBang = async () => {
const id = this.state.editorBang.id;
if (!id.startsWith('!')) {
return;
}
await browser.storage.local.remove(id);
const bangs = this.state.bangs;
const existingIndex = bangs.findIndex((bang) => bang.id === id);
if (existingIndex !== -1) {
bangs.splice(existingIndex, 1);
}
this.setState({
bangs,
editorBang: {...this.emptyBang},
});
};
saveBang = async () => {
const bang = this.state.editorBang;
try {
if (Bang.validate(bang)) {
const update: Record = {};
update[bang.id] = bang;
await browser.storage.local.set(update);
}
} catch (error: unknown) {
if (error instanceof Error) {
this.setState({
editorError: error.message,
});
} else {
throw error;
}
return;
}
const bangs = this.state.bangs;
const existingIndex = bangs.findIndex(({id}) => id === bang.id);
if (existingIndex === -1) {
bangs.push({...bang});
} else {
bangs[existingIndex] = {...bang};
}
this.setState({
bangs,
editorError: undefined,
});
};
render() {
const {bangs, editorError} = this.state;
const availableBangs = bangs.map((bang) => {
const onClick = () => {
this.setState({
editorBang: {...bang},
});
};
return html`
${bang.name}${bang.id}
`;
});
if (availableBangs.length === 0) {
availableBangs.push(
html`
You don't have any bangs yet, go add some!
`,
);
}
const parametersOrder: Array<[keyof BangParameters, string, string]> = [
['name', 'Name', 'Example'],
['id', 'Identifier', '!example'],
['baseUrl', 'Base Link', 'https://example.org'],
['searchUrl', 'Search Link', 'https://example.org/?search={{bang}}'],
];
const editorInputs: HtmComponent[] = [];
for (const [key, label, placeholder] of parametersOrder) {
const id = `bang-${key}`;
const value = this.state.editorBang[key];
const onInput = (event: Event) => {
this.editBang(event, key);
};
editorInputs.push(html`
${label}
`);
}
const validateError =
editorError === undefined
? undefined
: html`${editorError}
`;
return html`
Editor
${editorInputs}
Save
Remove
${validateError}
Your Bangs
How do I use Fangs?
Adding new Bangs:
Fill out all info in the Editor and click Save!
The "Identifier" is what you'll use to activate the bang in your
searches.
The "Base Link" is where you want to go when you don't include any
search terms.
The "Search Link" is where you want to go when you do include
something to search for, and the link must have "{{bang}}" in it
to insert your search text.
Editing existing Bangs:
Click on the Bang from the "Your Bangs" list to insert it into the
editor.
Editing a Bang with an Identifier that already exists will
overwrite it with the new one.
Removing Bangs:
Click on the Bang you want to remove and then click the Remove
button.
`;
}
}