diff --git a/source/options/components/user-labels.ts b/source/options/components/user-labels.ts index c0ea2c8..2731ee4 100644 --- a/source/options/components/user-labels.ts +++ b/source/options/components/user-labels.ts @@ -10,6 +10,10 @@ export function UserLabelsSetting(props: SettingProps): TRXComponent { person's profile is available, a [+] will be put next to it. Clicking on that will bring up a dialog to add a new label and clicking on existing labels will bring up the same dialog to edit them. +
+ Or you can use the dedicated${' '} + User Label Editor + to add, edit, or remove user labels.

diff --git a/source/options/user-label-editor.html b/source/options/user-label-editor.html new file mode 100644 index 0000000..70eb43c --- /dev/null +++ b/source/options/user-label-editor.html @@ -0,0 +1,22 @@ + + + + + + + Tildes ReExtended + + + + + + + + + + + + diff --git a/source/options/user-label-editor.ts b/source/options/user-label-editor.ts new file mode 100644 index 0000000..8399abe --- /dev/null +++ b/source/options/user-label-editor.ts @@ -0,0 +1,241 @@ +import {html} from 'htm/preact'; +import {Component, render} from 'preact'; + +import Settings from '../settings.js'; +import { + initializeGlobals, + isValidTildesUsername, + log, +} from '../utilities/exports.js'; + +window.addEventListener('load', async () => { + initializeGlobals(); + const settings = await Settings.fromSyncStorage(); + + render(html`<${App} settings=${settings} />`, document.body); +}); + +type Props = { + settings: Settings; +}; + +type State = { + hasUnsavedChanges: boolean; + newLabelUsername: string; + userLabels: UserLabel[]; +}; + +class App extends Component { + constructor(props: Props) { + super(props); + + this.state = { + hasUnsavedChanges: false, + newLabelUsername: '', + userLabels: props.settings.data.userLabels, + }; + } + + addNewLabel = () => { + const {newLabelUsername, userLabels} = this.state; + if (!isValidTildesUsername(newLabelUsername)) { + return; + } + + const existingUserLabel = userLabels.find( + ({username}) => username.toLowerCase() === newLabelUsername.toLowerCase(), + ); + + let id = 1; + if (userLabels.length > 0) { + id = userLabels.sort((a, b) => b.id - a.id)[0].id + 1; + } + + userLabels.push({ + color: '#ff00ff', + id, + priority: 0, + text: 'New Label', + username: existingUserLabel?.username ?? newLabelUsername, + }); + this.setState({userLabels}); + }; + + onNewUsernameInput = (event: Event) => { + const username = (event.target as HTMLInputElement).value; + this.setState({newLabelUsername: username}); + }; + + editUserLabel = (event: Event, targetId: number, key: keyof UserLabel) => { + const index = this.state.userLabels.findIndex(({id}) => id === targetId); + if (index === -1) { + log(`Tried to edit UserLabel with unknown ID: ${targetId}`); + return; + } + + const newValue = (event.target as HTMLInputElement).value; + if (key === 'id' || key === 'priority') { + this.state.userLabels[index][key] = Number(newValue); + } else { + this.state.userLabels[index][key] = newValue; + } + + this.setState({ + hasUnsavedChanges: true, + userLabels: this.state.userLabels, + }); + }; + + removeUserLabel = (targetId: number) => { + const userLabels = this.state.userLabels.filter(({id}) => id !== targetId); + this.setState({ + hasUnsavedChanges: true, + userLabels, + }); + }; + + saveUserLabels = () => { + const {settings} = this.props; + const {userLabels} = this.state; + settings.data.userLabels = userLabels; + void settings.save(); + this.setState({hasUnsavedChanges: false}); + }; + + render() { + const {hasUnsavedChanges, newLabelUsername, userLabels} = this.state; + userLabels.sort((a, b) => a.username.localeCompare(b.username)); + + const labelGroups: Map = new Map(); + for (const label of userLabels) { + const group = labelGroups.get(label.username) ?? []; + group.push(label); + labelGroups.set(label.username, group); + } + + const labels: TRXComponent[] = []; + for (const [username, group] of labelGroups) { + group.sort((a, b) => + a.priority === b.priority + ? a.text.localeCompare(b.text) + : b.priority - a.priority, + ); + const labelPreviews: TRXComponent[] = group.map( + ({color, text}) => html` + + ${text} + + `, + ); + + group.sort((a, b) => a.id - b.id); + const userLabels: TRXComponent[] = []; + for (const [index, label] of group.entries()) { + const textHandler = (event: Event) => { + this.editUserLabel(event, label.id, 'text'); + }; + + const colorHandler = (event: Event) => { + this.editUserLabel(event, label.id, 'color'); + }; + + const priorityHandler = (event: Event) => { + this.editUserLabel(event, label.id, 'priority'); + }; + + const removeHandler = () => { + this.removeUserLabel(label.id); + }; + + userLabels.push( + html` +
  • +
    + ${index === 0 ? html`` : undefined} + +
    + +
    + ${index === 0 ? html`` : undefined} + +
    + +
    + ${index === 0 ? html`` : undefined} + +
    + +
    + ${index === 0 ? html`` : undefined} + +
    +
  • + `, + ); + } + + labels.push(html` +
    +

    ${username} ${labelPreviews}

    +
      + ${userLabels} +
    +
    + `); + } + + return html` + + +
    +

    + To add a new label, enter the username for who you'd like to add the + label for, then press the Add New Label button. +
    + Changes are not automatically saved! +
    + If there are any unsaved changes an asterisk will appear in the Save + All Changes button. To undo all unsaved changes simply refresh the + page. +

    + +
    + + + + + +
    +
    ${labels}
    +
    + `; + } +} diff --git a/source/scss/_button.scss b/source/scss/_button.scss new file mode 100644 index 0000000..a5b94fb --- /dev/null +++ b/source/scss/_button.scss @@ -0,0 +1,21 @@ +@mixin button { + --button-color: var(--blue); + --button-color-alt: var(--dark-blue); + + background-color: var(--button-color); + border: none; + color: var(--foreground); + font-weight: bold; + min-width: 15rem; + padding: 8px 0; + + &:hover { + background-color: var(--button-color-alt); + cursor: pointer; + } + + &.destructive { + --button-color: var(--red); + --button-color-alt: var(--dark-red); + } +} diff --git a/source/scss/_settings.scss b/source/scss/_settings.scss index 8becd88..c7cd1e7 100644 --- a/source/scss/_settings.scss +++ b/source/scss/_settings.scss @@ -60,25 +60,7 @@ } .button { - --button-color: var(--blue); - --button-color-alt: var(--dark-blue); - - background-color: var(--button-color); - border: none; - color: var(--foreground); - font-weight: bold; - min-width: 15rem; - padding: 8px 0; - - &:hover { - background-color: var(--button-color-alt); - cursor: pointer; - } - - &.destructive { - --button-color: var(--red); - --button-color-alt: var(--dark-red); - } + @include button; } .import-export { diff --git a/source/scss/index.scss b/source/scss/index.scss index 2c51110..81f2ccd 100644 --- a/source/scss/index.scss +++ b/source/scss/index.scss @@ -1,6 +1,7 @@ @import 'reset'; @import 'variables'; @import 'colors'; +@import 'button'; html { font-size: 62.5%; @@ -47,7 +48,8 @@ details { .main-wrapper, .page-header, -.page-footer { +.page-footer, +.user-label-editor { margin-left: auto; margin-right: auto; width: $large-breakpoint; diff --git a/source/scss/user-label-editor.scss b/source/scss/user-label-editor.scss new file mode 100644 index 0000000..db7c5a7 --- /dev/null +++ b/source/scss/user-label-editor.scss @@ -0,0 +1,74 @@ +@import 'button'; + +.user-label-editor { + input { + background-color: var(--background-primary); + border: 1px solid var(--blue); + color: var(--foreground); + padding: 8px; + } + + .button { + @include button; + } + + .info { + border: 1px solid var(--blue); + margin-bottom: 8px; + padding: 8px; + } + + .main-controls { + display: flex; + gap: 8px; + margin-bottom: 8px; + } +} + +.groups { + display: grid; + gap: 8px; + grid-template-columns: repeat(1, 1fr); +} + +.group { + border: 1px solid var(--blue); + padding: 16px; + + h2 { + align-items: center; + display: flex; + gap: 8px; + margin-bottom: 16px; + } + + input { + width: 100%; + } + + label { + background-color: var(--blue); + display: flex; + flex-direction: column; + padding: 4px; + } + + li { + display: grid; + gap: 8px; + grid-template-columns: 40% 30% auto auto; + list-style: none; + } + + ul { + display: grid; + gap: 8px; + grid-template-columns: repeat(1, 1fr); + } +} + +.label-preview { + font-size: 60%; + padding: 4px 8px; + text-shadow: 0 0 2px #000, 0 0 4px #000; +} diff --git a/vite.config.ts b/vite.config.ts index ecce248..20bfb55 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ plugins: [ preact(), webExtension({ + additionalInputs: ['options/user-label-editor.html'], assets: 'assets', browser: 'firefox', manifest: path.join(sourceDir, 'manifest.json'),