1
Fork 0

Create the dedicated User Label Editor (#1).

This commit is contained in:
Bauke 2022-02-27 15:37:11 +01:00
parent 5cf6aa997a
commit e3e758f7ba
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
8 changed files with 367 additions and 20 deletions

View File

@ -10,6 +10,10 @@ export function UserLabelsSetting(props: SettingProps): TRXComponent {
person's profile is available, a <code>[+]</code> will be put next to person's profile is available, a <code>[+]</code> will be put next to
it. Clicking on that will bring up a dialog to add a new label and 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. clicking on existing labels will bring up the same dialog to edit them.
<br />
Or you can use the dedicated${' '}
<a href="./user-label-editor.html">User Label Editor</a>
to add, edit, or remove user labels.
</p> </p>
<details> <details>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tildes ReExtended</title>
<link rel="shortcut icon" href="../assets/tildes-reextended-128.png"
type="image/png">
<link rel="stylesheet" href="../scss/modern-normalize.scss">
<link rel="stylesheet" href="../scss/index.scss">
<link rel="stylesheet" href="../scss/user-label-editor.scss">
</head>
<body>
<noscript>
This web extension does not work without JavaScript, sorry. :(
</noscript>
<script type="module" src="./user-label-editor.ts"></script>
</body>
</html>

View File

@ -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<Props, State> {
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<string, UserLabel[]> = 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`
<span style=${{background: color}} class="label-preview">
${text}
</span>
`,
);
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`
<li key=${label.id}>
<div>
${index === 0 ? html`<label>Text</label>` : undefined}
<input
onInput=${textHandler}
placeholder="Text"
value=${label.text}
/>
</div>
<div>
${index === 0 ? html`<label>Color</label>` : undefined}
<input
onInput=${colorHandler}
placeholder="Color"
value=${label.color}
/>
</div>
<div>
${index === 0 ? html`<label>Priority</label>` : undefined}
<input
onInput=${priorityHandler}
placeholder="Priority"
type="number"
value=${label.priority}
/>
</div>
<div>
${index === 0 ? html`<label>Controls</label>` : undefined}
<button class="button destructive" onClick=${removeHandler}>
Remove
</button>
</div>
</li>
`,
);
}
labels.push(html`
<div class="group">
<h2>${username} ${labelPreviews}</h2>
<ul>
${userLabels}
</ul>
</div>
`);
}
return html`
<header class="page-header">
<h1>
<img src="/assets/tildes-reextended-128.png" />
User Label Editor
</h1>
</header>
<main class="page-main user-label-editor">
<p class="info">
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.
<br />
<b>Changes are not automatically saved!</b>
<br />
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.
</p>
<div class="main-controls">
<input
onInput=${this.onNewUsernameInput}
placeholder="Username"
value=${newLabelUsername}
/>
<button class="button" onClick=${this.addNewLabel}>
Add New Label
</button>
<button class="button" onClick=${this.saveUserLabels}>
Save All Changes${hasUnsavedChanges ? '*' : ''}
</button>
</div>
<div class="groups">${labels}</div>
</main>
`;
}
}

21
source/scss/_button.scss Normal file
View File

@ -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);
}
}

View File

@ -60,25 +60,7 @@
} }
.button { .button {
--button-color: var(--blue); @include button;
--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);
}
} }
.import-export { .import-export {

View File

@ -1,6 +1,7 @@
@import 'reset'; @import 'reset';
@import 'variables'; @import 'variables';
@import 'colors'; @import 'colors';
@import 'button';
html { html {
font-size: 62.5%; font-size: 62.5%;
@ -47,7 +48,8 @@ details {
.main-wrapper, .main-wrapper,
.page-header, .page-header,
.page-footer { .page-footer,
.user-label-editor {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: $large-breakpoint; width: $large-breakpoint;

View File

@ -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;
}

View File

@ -18,6 +18,7 @@ export default defineConfig({
plugins: [ plugins: [
preact(), preact(),
webExtension({ webExtension({
additionalInputs: ['options/user-label-editor.html'],
assets: 'assets', assets: 'assets',
browser: 'firefox', browser: 'firefox',
manifest: path.join(sourceDir, 'manifest.json'), manifest: path.join(sourceDir, 'manifest.json'),