import { ConfirmButton, FeedbackButton, PrivacyLink, } from '@holllo/preact-components'; import {html} from 'htm/preact'; import {Component} from 'preact'; import browser from 'webextension-polyfill'; import Bang, {type 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) .sort((a, b) => a.id.localeCompare(b.id)); 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 false to make the FeedbackButton not show feedback. return false; } 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 active = bang.id === this.state.editorBang.id ? 'active' : ''; const onClick = () => { const allEqual = Object.entries(this.state.editorBang).every( ([key, value]) => bang[key as keyof BangParameters] === value, ); if (allEqual) { this.setState({ editorBang: {...this.emptyBang}, }); } else { this.setState({ editorBang: {...bang}, }); } document.querySelector('.bang-editor')?.setAttribute('open', 'true'); }; return html`
  • `; }); 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`
    `); } const validateError = editorError === undefined ? undefined : html`

    ${editorError}

    `; const testLinkAttributes: Record = { class: 'button', }; if (this.state.editorBang.searchUrl.includes('{{bang}}')) { testLinkAttributes.href = this.state.editorBang.searchUrl.replace( /{{bang}}/g, 'test', ); } else { testLinkAttributes.disabled = true; } return html`
    Editor
    ${editorInputs}
    <${PrivacyLink} attributes=${testLinkAttributes}>Test <${FeedbackButton} attributes=${{class: 'button'}} click=${this.saveBang} feedbackText="Saved" text="Save" timeout=${5 * 1000} /> <${ConfirmButton} class="button destructive" click=${this.removeBang} confirmClass="confirm" confirmText="Confirm Removal" text="Remove ${this.state.editorBang.id}" timeout=${5 * 1000} />
    ${validateError}
    Your Bangs
      ${availableBangs}
    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.
    `; } }