Create the dedicated User Label Editor (#1).
This commit is contained in:
parent
5cf6aa997a
commit
e3e758f7ba
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
|
Loading…
Reference in New Issue