1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-02-06 23:32:58 +00:00

21 Commits
0.1.0 ... 0.1.2

Author SHA1 Message Date
7eab8d633f Bumping version to 0.1.2 2024-07-05 02:12:59 +04:00
68de994811 Merge pull request #13 from koloml/prevent-closing-tab-before-submission-completed
Prompt the browser confirmation popup when attempting to change or close the tab before all submissions are processed
2024-07-05 02:05:59 +04:00
be4aec54fe Merge pull request #12 from koloml/disable-search-autocompletion
Removing auto-completion logic
2024-07-05 02:05:43 +04:00
9ca663ffdb Added icon for failed status 2024-07-05 02:00:36 +04:00
bb0f84c9ad Showing exit popup when leaving page before submissions are processed 2024-07-05 01:57:34 +04:00
c8eb54ab98 Removing auto-completion logic 2024-07-05 01:06:11 +04:00
d2140c6eee Bumping version to 0.1.1 2024-06-27 02:04:45 +04:00
741bc71f11 Merge pull request #8 from koloml/minor-text-and-styling-changes
Minor content & styling changes and fixes
2024-06-27 02:03:07 +04:00
9732fa2005 Adding more spaces between elements on profile view 2024-06-27 02:01:46 +04:00
c45d4619a8 Renaming main link to "Tagging Profiles" 2024-06-27 02:01:10 +04:00
e42419e3c5 Merge pull request #7 from koloml/tags-editor-refactor
Re-import base input styling removed with Tags Editor styling back to main popup stylesheet
2024-06-27 01:43:15 +04:00
5c48e1cca6 Re-import base input styling to popup stylesheet
Forgot to move import out of tags editor into the main popup stylesheet.
2024-06-27 01:41:17 +04:00
c55b9cc851 Merge pull request #6 from koloml/media-boxes-positions
Fixed Maintenance popups getting out of screen on the start and on the end of the grid rows
2024-06-27 01:37:44 +04:00
723e72b65f Merge pull request #5 from koloml/tags-editor-refactor
Refactoring Tags Editor from Web Components API to Svelte component, improved accessibility for keyboards
2024-06-27 01:37:31 +04:00
8da814c8dd Moving popups of first/last media boxes to not get out of screen 2024-06-27 01:28:53 +04:00
4bd7a67a03 Calculate the first/last media boxes in the row on every resize 2024-06-27 01:12:04 +04:00
b302e8fbb7 Removed tags editor stylesheets from global styles 2024-06-27 00:44:38 +04:00
e6e537ea0c Tags Editor: Removed Web Component with a component made with Svelte
Additionally, this editor works a little bit better when used with
keyboard. It supports tabbing between "remove" buttons inside the tags
and pressing them with Space/Enter.

Web Component was an idea to keep the editor the same between frontend
and Svelte app, but as I figured out later, extensions can't use those.
Unfortunate.
2024-06-27 00:43:29 +04:00
15d318ec90 Merge pull request #1 from koloml/fullscreen-viewer-interactions
Support playing videos in Fullscreen Viewer, closing Viewer on click
2024-06-23 18:26:21 +04:00
a81a7c5d27 Support video content in Fullscreen Viewer 2024-06-23 18:25:05 +04:00
eda7342144 Closing full screen viewer on click 2024-06-23 15:07:16 +04:00
19 changed files with 301 additions and 474 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "Furbooru Tagging Assistant",
"description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.",
"version": "0.1.0",
"version": "0.1.2",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
@@ -26,17 +26,6 @@
"css": [
"src/styles/content/listing.scss"
]
},
{
"matches": [
"*://*.furbooru.org/*"
],
"js": [
"src/content/header.js"
],
"css": [
"src/styles/content/header.scss"
]
}
],
"action": {

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.1.0",
"version": "0.1.2",
"private": true,
"scripts": {
"build": "npm run build:popup && npm run build:extension",

View File

@@ -1,21 +1,94 @@
<script>
import "$lib/web-components/TagEditorComponent.js";
/**
* List of tags to edit. Any duplicated tags present in the array will be removed on the first edit.
* @type {string[]}
*/
export let tags = [];
let tagsAttribute = tags.join(',');
/** @type {Set<string>} */
let uniqueTags = new Set();
$: uniqueTags = new Set(tags);
/** @type {string} */
let addedTagName = '';
/**
* @param {CustomEvent<string[]>} event
* Create a callback function to pass into both mouse & keyboard events for tag removal.
* @param {string} tagName
* @return {function(Event)} Callback to pass as event listener.
*/
function onTagsChanged(event) {
tags = event.detail;
function createTagRemoveHandler(tagName) {
return event => {
if (event.type === 'click') {
removeTag(tagName);
}
if (event instanceof KeyboardEvent && (event.code === 'Enter' || event.code === 'Space')) {
// To be more comfortable, automatically focus next available tag's remove button in the list.
if (event.currentTarget instanceof HTMLElement) {
const currenTagElement = event.currentTarget.closest('.tag');
const nextTagElement = currenTagElement?.previousElementSibling ?? currenTagElement?.parentElement?.firstElementChild;
const nextRemoveButton = nextTagElement?.querySelector('.remove');
if (nextRemoveButton instanceof HTMLElement) {
nextRemoveButton.focus();
}
}
removeTag(tagName);
}
}
}
$: tagsAttribute = tags.join(',');
/**
* @param {string} tagName
*/
function removeTag(tagName) {
uniqueTags.delete(tagName);
tags = Array.from(uniqueTags);
}
/**
* @param {string} tagName
*/
function addTag(tagName) {
uniqueTags.add(tagName);
tags = Array.from(uniqueTags);
}
/**
* Handle adding new tags to the list or removing them when backspace is pressed.
* @param {KeyboardEvent} event
*/
function handleKeyPresses(event) {
if (event.code === 'Enter' && addedTagName.length) {
addTag(addedTagName)
addedTagName = '';
}
if (event.code === 'Backspace' && !addedTagName.length && tags?.length) {
removeTag(tags[tags.length - 1]);
}
}
</script>
<tags-editor tags="{tagsAttribute}" on:change={onTagsChanged}></tags-editor>
<div class="tags-editor">
{#each uniqueTags.values() as tagName}
<div class="tag">
{tagName}
<span class="remove" on:click={createTagRemoveHandler(tagName)}
on:keydown={createTagRemoveHandler(tagName)}
role="button" tabindex="0">x</span>
</div>
{/each}
<input type="text" bind:value={addedTagName} on:keydown={handleKeyPresses}/>
</div>
<style lang="scss">
.tags-editor {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
</style>

View File

@@ -1,7 +0,0 @@
import {initializeSiteHeader} from "$lib/components/SiteHeaderWrapper.js";
const siteHeader = document.querySelector('.header');
if (siteHeader) {
initializeSiteHeader(siteHeader);
}

View File

@@ -1,10 +1,13 @@
import {createMaintenancePopup} from "$lib/components/MaintenancePopup.js";
import {createMediaBoxTools} from "$lib/components/MediaBoxTools.js";
import {initializeMediaBox} from "$lib/components/MediaBoxWrapper.js";
import {calculateMediaBoxesPositions, initializeMediaBox} from "$lib/components/MediaBoxWrapper.js";
import {createMaintenanceStatusIcon} from "$lib/components/MaintenanceStatusIcon.js";
import {createImageShowFullscreenButton} from "$lib/components/ImageShowFullscreenButton.js";
document.querySelectorAll('.media-box').forEach(mediaBoxElement => {
/** @type {NodeListOf<HTMLElement>} */
const mediaBoxes = document.querySelectorAll('.media-box');
mediaBoxes.forEach(mediaBoxElement => {
initializeMediaBox(mediaBoxElement, [
createMediaBoxTools(
createMaintenancePopup(),
@@ -18,3 +21,5 @@ document.querySelectorAll('.media-box').forEach(mediaBoxElement => {
window.dispatchEvent(new CustomEvent('resize'));
})
});
calculateMediaBoxesPositions(mediaBoxes);

View File

@@ -24,10 +24,34 @@ export class ImageShowFullscreenButton extends BaseComponent {
#onButtonClicked() {
const imageViewer = ImageShowFullscreenButton.#resolveFullscreenViewer();
let imageElement = imageViewer.querySelector('img') ?? document.createElement('img');
const largeSourceUrl = this.#mediaBoxTools.mediaBox.imageLinks.large;
imageElement.src = this.#mediaBoxTools.mediaBox.imageLinks.large;
imageViewer.appendChild(imageElement);
let imageElement = imageViewer.querySelector('img');
let videoElement = imageViewer.querySelector('video');
if (imageElement) {
imageElement.remove();
}
if (videoElement) {
videoElement.remove();
}
if (largeSourceUrl.endsWith('.webm') || largeSourceUrl.endsWith('.mp4')) {
videoElement ??= document.createElement('video');
videoElement.src = largeSourceUrl;
videoElement.volume = 0;
videoElement.autoplay = true;
videoElement.loop = true;
videoElement.controls = true;
imageViewer.appendChild(videoElement);
} else {
imageElement ??= document.createElement('img');
imageElement.src = largeSourceUrl;
imageViewer.appendChild(imageElement);
}
imageViewer.classList.add('shown');
}
@@ -57,12 +81,38 @@ export class ImageShowFullscreenButton extends BaseComponent {
document.addEventListener('keydown', event => {
// When ESC pressed
if (event.code === 'Escape' || event.code === 'Esc') {
element.classList.remove('shown');
this.#closeFullscreenViewer(element);
}
});
element.addEventListener('click', () => {
this.#closeFullscreenViewer(element);
});
return element;
}
/**
* @param {HTMLElement} [viewerElement]
*/
static #closeFullscreenViewer(viewerElement = null) {
viewerElement ??= this.#resolveFullscreenViewer();
viewerElement.classList.remove('shown');
/** @type {HTMLVideoElement} */
const videoElement = viewerElement.querySelector('video');
if (!videoElement) {
return;
}
// Stopping and muting the video
requestAnimationFrame(() => {
videoElement.volume = 0;
videoElement.pause();
videoElement.remove();
})
}
}
export function createImageShowFullscreenButton() {

View File

@@ -151,6 +151,11 @@ export class MaintenancePopup extends BaseComponent {
}
if (this.#tagsToAdd.size || this.#tagsToRemove.size) {
// Notify only once, when first planning to submit
if (!this.#isPlanningToSubmit) {
MaintenancePopup.#notifyAboutPendingSubmission(true);
}
this.#isPlanningToSubmit = true;
this.emit('maintenance-state-change', 'waiting');
}
@@ -181,20 +186,32 @@ export class MaintenancePopup extends BaseComponent {
this.emit('maintenance-state-change', 'processing');
const maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags(
this.#mediaBoxTools.mediaBox.imageId,
tagsList => {
for (let tagName of this.#tagsToRemove) {
tagsList.delete(tagName);
}
let maybeTagsAndAliasesAfterUpdate;
for (let tagName of this.#tagsToAdd) {
tagsList.add(tagName);
}
try {
maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags(
this.#mediaBoxTools.mediaBox.imageId,
tagsList => {
for (let tagName of this.#tagsToRemove) {
tagsList.delete(tagName);
}
return tagsList;
}
);
for (let tagName of this.#tagsToAdd) {
tagsList.add(tagName);
}
return tagsList;
}
);
} catch (e) {
console.warn('Tags submission failed:', e);
MaintenancePopup.#notifyAboutPendingSubmission(false);
this.emit('maintenance-state-change', 'failed');
this.#isSubmitting = false;
return;
}
if (maybeTagsAndAliasesAfterUpdate) {
this.emit('tags-updated', maybeTagsAndAliasesAfterUpdate);
@@ -206,6 +223,7 @@ export class MaintenancePopup extends BaseComponent {
this.#tagsToRemove.clear();
this.#refreshTagsList();
MaintenancePopup.#notifyAboutPendingSubmission(false);
this.#isSubmitting = false;
}
@@ -276,9 +294,42 @@ export class MaintenancePopup extends BaseComponent {
}
}
/**
* Notify the frontend about new pending submission started.
* @param {boolean} isStarted True if started, false if ended.
*/
static #notifyAboutPendingSubmission(isStarted) {
if (this.#pendingSubmissionCount === null) {
this.#pendingSubmissionCount = 0;
this.#initializeExitPromptHandler();
}
this.#pendingSubmissionCount += isStarted ? 1 : -1;
}
/**
* Subscribe to the global window closing event, show the prompt when there are pending submission.
*/
static #initializeExitPromptHandler() {
window.addEventListener('beforeunload', event => {
if (!this.#pendingSubmissionCount) {
return;
}
event.preventDefault();
event.returnValue = true;
});
}
static #scrapedAPI = new ScrapedAPI();
static #delayBeforeSubmissionMs = 500;
/**
* Amount of pending submissions or NULL if logic was not yet initialized.
* @type {number|null}
*/
static #pendingSubmissionCount = null;
}
export function createMaintenancePopup() {

View File

@@ -38,7 +38,11 @@ export class MaintenanceStatusIcon extends BaseComponent {
break;
case "complete":
this.container.innerText = '✅'
this.container.innerText = '✅';
break;
case "failed":
this.container.innerText = '⚠️';
break;
default:

View File

@@ -78,6 +78,29 @@ export function initializeMediaBox(mediaBoxContainer, childComponentElements) {
}
}
/**
* @param {NodeListOf<HTMLElement>} mediaBoxesList
*/
export function calculateMediaBoxesPositions(mediaBoxesList) {
window.addEventListener('resize', () => {
/** @type {HTMLElement|null} */
let lastMediaBox = null,
/** @type {number|null} */
lastMediaBoxPosition = null;
for (let mediaBoxElement of mediaBoxesList) {
const yPosition = mediaBoxElement.getBoundingClientRect().y;
const isOnTheSameLine = yPosition === lastMediaBoxPosition;
mediaBoxElement.classList.toggle('media-box--first', !isOnTheSameLine);
lastMediaBox?.classList.toggle('media-box--last', !isOnTheSameLine);
lastMediaBox = mediaBoxElement;
lastMediaBoxPosition = yPosition;
}
})
}
/**
* @typedef {Object} ImageURIs
* @property {string} full

View File

@@ -1,186 +0,0 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {QueryLexer, QuotedTermToken, TermToken, Token} from "$lib/booru/search/QueryLexer.js";
export class SearchWrapper extends BaseComponent {
/** @type {HTMLInputElement|null} */
#searchField = null;
/** @type {HTMLInputElement|null} */
#autoCompleteField = null;
/** @type {string|null} */
#lastParsedSearchValue = null;
/** @type {Token[]} */
#cachedParsedQuery = [];
build() {
this.container.classList.add('header__search--completable');
this.#searchField = this.container.querySelector('input[name=q]');
this.#searchField.autocomplete = 'off'; // Browser's auto-complete will get in the way!
const autoCompleteField = document.createElement('input');
autoCompleteField.dataset.ac = 'true';
autoCompleteField.dataset.acMinLength = '3';
autoCompleteField.dataset.acSource = '/autocomplete/tags?term=';
autoCompleteField.classList.add('search-autocomplete-dummy');
this.#autoCompleteField = autoCompleteField;
this.container.appendChild(autoCompleteField);
}
init() {
this.#searchField.addEventListener('input', this.#updateAutoCompletedFragment.bind(this));
this.#searchField.addEventListener('keydown', this.#onSearchFieldKeyPressed.bind(this));
this.#searchField.addEventListener('selectionchange', this.#updateAutoCompletedFragment.bind(this));
}
#updateAutoCompletedFragment() {
const searchableFragment = this.#findCurrentTagFragment();
this.#emitAutoComplete(searchableFragment || '');
}
#getInputUserSelection() {
return Math.min(
this.#searchField.selectionStart,
this.#searchField.selectionEnd
);
}
#resolveQueryTokens() {
const searchValue = this.#searchField.value;
if (searchValue === this.#lastParsedSearchValue && this.#cachedParsedQuery) {
return this.#cachedParsedQuery;
}
this.#lastParsedSearchValue = searchValue;
this.#cachedParsedQuery = new QueryLexer(searchValue).parse();
return this.#cachedParsedQuery;
}
/**
* @param {KeyboardEvent} event
*/
#onSearchFieldKeyPressed(event) {
// On enter, attempt to replace the current active tag in the query with autocomplete selection
if (event.code === 'Enter') {
this.#onEnterPressed(event);
}
this.#autoCompleteField.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: event.keyCode
})
);
// Similarly to the site's autocomplete logic, we need to prevent the arrows up/down from causing any issues
if (event.keyCode === 38 || event.keyCode === 40) {
event.preventDefault();
}
}
/**
* @param {KeyboardEvent} event
*/
#onEnterPressed(event) {
const autocompleteSelection = document.querySelector('.autocomplete__item--selected');
if (!autocompleteSelection) {
return;
}
const activeToken = SearchWrapper.#findActiveSearchTermPosition(
this.#resolveQueryTokens(),
this.#getInputUserSelection(),
);
if (activeToken instanceof TermToken || activeToken instanceof QuotedTermToken) {
const selectionStart = activeToken.index;
const selectionEnd = activeToken.index + activeToken.value.length;
let autocompletedValue = autocompleteSelection.dataset.value;
if (activeToken instanceof QuotedTermToken) {
autocompletedValue = `"${QuotedTermToken.encode(autocompletedValue)}"`;
}
this.#searchField.value = this.#searchField.value.slice(0, selectionStart)
+ autocompletedValue
+ this.#searchField.value.slice(selectionEnd);
const newSelectionEnd = selectionStart + autocompletedValue.length;
// Place the caret at the end of the currently active tag.
// Actually, this does not work for some reason. After the tag is sent to the field and selection was changed to
// the end of the inserted tag, browser just does not scroll the input to the caret position.
this.#searchField.focus();
this.#searchField.setSelectionRange(newSelectionEnd, newSelectionEnd);
event.preventDefault();
}
}
/**
* @return {string|null}
*/
#findCurrentTagFragment() {
if (!this.#searchField) {
return null;
}
let searchValue = this.#searchField.value;
if (!searchValue) {
return null;
}
const token = SearchWrapper.#findActiveSearchTermPosition(
this.#resolveQueryTokens(),
this.#getInputUserSelection(),
);
if (token instanceof TermToken) {
return token.value;
}
if (token instanceof QuotedTermToken) {
return token.decodedValue;
}
return searchValue;
}
#emitAutoComplete(userInputFragment) {
this.#autoCompleteField.value = userInputFragment;
// Should be at least one frame away, since input event always removes autocomplete window
requestAnimationFrame(() => {
this.#autoCompleteField.dispatchEvent(
new InputEvent('input', {bubbles: true})
);
const autocompleteContainer = document.querySelector('.autocomplete');
if (autocompleteContainer) {
autocompleteContainer.style.left = `${this.container.offsetLeft}px`;
}
});
}
/**
* Loosely estimate where current selected search term is located and return it if found.
* @param {Token[]} tokens Search value to find the actively selected term from.
* @param {number} userSelectionIndex The index of the user selection.
* @return {Token|null} Search term object or NULL if nothing found.
*/
static #findActiveSearchTermPosition(tokens, userSelectionIndex) {
return tokens.find(
token => token.index < userSelectionIndex && token.index + token.value.length >= userSelectionIndex
);
}
}
export function initializeSearWrapper(formElement) {
new SearchWrapper(formElement).initialize();
}

View File

@@ -1,23 +0,0 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {SearchWrapper} from "$lib/components/SearchWrapper.js";
class SiteHeaderWrapper extends BaseComponent {
/** @type {SearchWrapper|null} */
#searchWrapper = null;
build() {
const searchForm = this.container.querySelector('.header__search');
this.#searchWrapper = searchForm && new SearchWrapper(searchForm) || null;
}
init() {
if (this.#searchWrapper) {
this.#searchWrapper.initialize();
}
}
}
export function initializeSiteHeader(siteHeaderElement) {
new SiteHeaderWrapper(siteHeaderElement)
.initialize();
}

View File

@@ -1,184 +0,0 @@
export default class TagEditorComponent extends HTMLElement {
/**
* Array of elements representing tags.
* @type {HTMLElement[]}
*/
#tagElements = [];
/**
* Generated input for adding new tags to the tag list. Will be rendered on connecting.
* @type {HTMLInputElement|undefined}
*/
#tagInput;
/**
* Cached list of tag names. Changing this value will not automatically change the actual tags.
* @type {Set<string>}
*/
#tagsSet = new Set();
constructor() {
super();
}
connectedCallback() {
if (!this.#tagInput) {
this.#tagInput = document.createElement('input');
this.appendChild(this.#tagInput);
this.#tagInput.addEventListener('keydown', this.#onKeyDownDetectActions.bind(this));
}
if (!this.#tagElements.length) {
this.#renderTags();
}
this.addEventListener('click', this.#onClickDetectTagRemoval.bind(this));
}
/**
* Render the list of tag elements based on the tag attribute. Should be called every time tag attribute is changed.
*/
#renderTags() {
const tags = this.getAttribute(TagEditorComponent.#tagsAttribute) || '';
const updatedTagsSet = new Set(
tags.split(',')
.map(tagName => tagName.trim())
.filter(Boolean)
);
this.#tagsSet = new Set(updatedTagsSet.values());
this.#tagElements = this.#tagElements.filter(tagElement => {
const tagName = tagElement.dataset.tag;
if (!updatedTagsSet.has(tagName)) {
tagElement.remove();
return false;
}
updatedTagsSet.delete(tagName);
return true;
});
for (let tagName of updatedTagsSet) {
const tagElement = document.createElement('div');
tagElement.classList.add('tag');
tagElement.innerText = tagName;
tagElement.dataset.tag = tagName;
const tagRemoveElement = document.createElement("span");
tagRemoveElement.classList.add('remove');
tagRemoveElement.innerText = 'x';
tagElement.appendChild(tagRemoveElement);
this.#tagInput.insertAdjacentElement('beforebegin', tagElement);
this.#tagElements.push(tagElement);
}
}
/**
* Detect add/remove keyboard shortcuts on the input.
* @param {KeyboardEvent} event
*/
#onKeyDownDetectActions(event) {
const isTagSubmit = event.key === 'Enter';
const isTagRemove = event.key === 'Backspace' && !this.#tagInput.value.length;
if (!isTagSubmit && !isTagRemove) {
return;
}
if (isTagSubmit) {
event.preventDefault();
}
const providedTagName = this.#tagInput.value.trim();
if (providedTagName && isTagSubmit) {
if (!this.#tagsSet.has(providedTagName)) {
this.setAttribute(
TagEditorComponent.#tagsAttribute,
[...this.#tagsSet, providedTagName].join(',')
);
}
this.#tagInput.value = '';
return;
}
if (isTagRemove && this.#tagsSet.size) {
this.setAttribute(
TagEditorComponent.#tagsAttribute,
[...this.#tagsSet].slice(0, -1).join(',')
)
}
}
/**
* Detect clicks on the "remove" button inside tags.
* @param {MouseEvent} event
*/
#onClickDetectTagRemoval(event) {
/** @type {HTMLElement} */
const maybeRemoveTagElement = event.target;
if (!maybeRemoveTagElement.classList.contains('remove')) {
return;
}
/** @type {HTMLElement} */
const tagElement = maybeRemoveTagElement.closest('.tag');
if (!tagElement) {
return;
}
const tagName = tagElement.dataset.tag;
if (this.#tagsSet.has(tagName)) {
this.#tagsSet.delete(tagName);
this.setAttribute(
TagEditorComponent.#tagsAttribute,
[...this.#tagsSet].join(",")
);
}
}
/**
* @param {string} name
* @param {string} from
* @param {string} to
*/
attributeChangedCallback(name, from, to) {
if (!this.isConnected) {
return;
}
if (name === TagEditorComponent.#tagsAttribute) {
this.#renderTags();
this.dispatchEvent(
new CustomEvent(
'change',
{
detail: [...this.#tagsSet.values()]
}
)
);
}
}
static get observedAttributes() {
return [this.#tagsAttribute];
}
static #tagsAttribute = 'tags';
}
if (!customElements.get('tags-editor')) {
customElements.define('tags-editor', TagEditorComponent);
} else {
console.warn('Tags Component is attempting to initialize twice!');
}

View File

@@ -4,7 +4,7 @@
</script>
<Menu>
<MenuLink href="/settings/maintenance">Manual Tags Maintenance</MenuLink>
<MenuLink href="/settings/maintenance">Tagging Profiles</MenuLink>
<hr>
<MenuLink href="/about">About</MenuLink>
</Menu>

View File

@@ -6,5 +6,5 @@
<Menu>
<MenuLink href="/">Back</MenuLink>
<hr>
<MenuLink href="/settings/maintenance">Maintenance</MenuLink>
<MenuLink href="/settings/maintenance">Tagging Profiles</MenuLink>
</Menu>

View File

@@ -43,12 +43,12 @@
<hr>
</Menu>
{#if profile}
<div>
<strong>Profile:</strong><br>
{profile.settings.name}
<div class="block">
<strong>Profile:</strong>
<div>{profile.settings.name}</div>
</div>
<div>
<strong>Focused Tags:</strong>
<div class="block">
<strong>Tags:</strong>
<div class="tags-list">
{#each profile.settings.tags as tagName}
<span class="tag">{tagName}</span>
@@ -67,10 +67,20 @@
{/if}
</MenuLink>
</Menu>
<style>
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.block + .block {
margin-top: .5em;
strong {
display: block;
margin-bottom: .25em;
}
}
</style>

View File

@@ -1,11 +0,0 @@
.header__search--completable {
.search-autocomplete-dummy {
position: absolute;
height: 0;
opacity: 0;
pointer-events: none;
padding: 0;
margin: 0;
align-self: flex-end;
}
}

View File

@@ -95,6 +95,51 @@
display: none;
}
&.media-box--first:not(.media-box--last) {
.media-box-tools:before {
left: -1px;
}
.media-box-tools:after {
right: -75%;
}
.maintenance-popup {
left: -1px;
right: -75%;
}
}
&.media-box--last:not(.media-box--first) {
.media-box-tools:before {
left: -75%;
}
.media-box-tools:after {
right: -1px;
}
.maintenance-popup {
left: -75%;
right: -1px;
}
}
&.media-box--last.media-box--first {
.media-box-tools:before {
left: -1px;
}
.media-box-tools:after {
right: -1px;
}
.maintenance-popup {
left: -1px;
right: -1px;
}
}
&:hover {
.media-box-tools.has-active-profile {
&:before, &:after {
@@ -127,7 +172,7 @@
justify-content: stretch;
align-items: stretch;
img {
img, video {
object-fit: contain;
width: 100%;
height: 100%;

View File

@@ -1,12 +0,0 @@
@use '../colors';
@import "input";
tags-editor {
display: flex;
gap: 5px;
flex-wrap: wrap;
input {
width: 180px;
}
}

View File

@@ -30,6 +30,6 @@ a {
}
}
@import "injectable/tags-editor";
@import "injectable/input";
@import "injectable/tag";
@import "injectable/icons";
@import "injectable/icons";