Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
096314933d | ||
|
|
05c890ee37 | ||
|
|
d4725dfebc | ||
|
|
c4d5eee89d | ||
|
|
fa4d87ee77 | ||
|
|
3cb5a41cc8 | ||
|
|
16a325b798 | ||
|
|
e3171940fb |
+11
-19
@@ -235,16 +235,7 @@ class Downloader:
|
|||||||
|
|
||||||
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
||||||
"""Checks HuggingFace for file updates and updates metadata."""
|
"""Checks HuggingFace for file updates and updates metadata."""
|
||||||
perform_hf_check = True
|
perform_hf_check = force_check
|
||||||
if not force_check and dataset_meta.get("last_remote_check_timestamp"):
|
|
||||||
try:
|
|
||||||
last_check_dt = datetime.fromisoformat(dataset_meta["last_remote_check_timestamp"])
|
|
||||||
if now_utc - last_check_dt < timedelta(days=7):
|
|
||||||
perform_hf_check = False
|
|
||||||
except (ValueError, KeyError, TypeError):
|
|
||||||
print(
|
|
||||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check."
|
|
||||||
)
|
|
||||||
|
|
||||||
if perform_hf_check:
|
if perform_hf_check:
|
||||||
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
||||||
@@ -303,15 +294,16 @@ class Downloader:
|
|||||||
return f"File {file_name} is missing or empty locally."
|
return f"File {file_name} is missing or empty locally."
|
||||||
|
|
||||||
# Check if the last modified date on HuggingFace is newer than the last download date
|
# Check if the last modified date on HuggingFace is newer than the last download date
|
||||||
try:
|
# Temporarily comment out until changes to remote files can be detected
|
||||||
last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
# try:
|
||||||
hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
# last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
||||||
if hf_modified_dt > last_download_dt:
|
# hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
||||||
return f"Remote file {file_name} is newer (HF: {last_download_dt}, Local Download: {hf_modified_dt})."
|
# if hf_modified_dt > last_download_dt:
|
||||||
except (ValueError, TypeError):
|
# return f"Remote file {file_name} is newer (HF: {hf_modified_dt}, Local Download: {last_download_dt})."
|
||||||
file_meta_entry["last_download"] = None
|
# except (ValueError, TypeError):
|
||||||
file_meta_entry["last_modified_on_hf"] = None
|
# file_meta_entry["last_download"] = None
|
||||||
return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
# file_meta_entry["last_modified_on_hf"] = None
|
||||||
|
# return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
||||||
|
|
||||||
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
||||||
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-autocomplete-plus"
|
name = "comfyui-autocomplete-plus"
|
||||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["",]
|
dependencies = ["",]
|
||||||
|
|
||||||
|
|||||||
@@ -97,10 +97,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item {
|
.autocomplete-plus-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -292,10 +296,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#related-tags-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.related-tag-item {
|
.related-tag-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
+41
-26
@@ -17,6 +17,7 @@ import {
|
|||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
|
getScrollbarWidth,
|
||||||
IconSvgHtmlString
|
IconSvgHtmlString
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { settingValues } from './settings.js';
|
import { settingValues } from './settings.js';
|
||||||
@@ -311,10 +312,10 @@ function insertTagToTextArea(inputElement, tagDataToInsert) {
|
|||||||
let replaceEnd = cursorPos;
|
let replaceEnd = cursorPos;
|
||||||
|
|
||||||
let normalizedTag;
|
let normalizedTag;
|
||||||
if(Object.values(ModelTagSource).includes(tagDataToInsert.source)){
|
if (Object.values(ModelTagSource).includes(tagDataToInsert.source)) {
|
||||||
// If the tag is from a model tag source, don't want to normalize it
|
// If the tag is from a model tag source, don't want to normalize it
|
||||||
normalizedTag = tagDataToInsert.tag;
|
normalizedTag = tagDataToInsert.tag;
|
||||||
}else{
|
} else {
|
||||||
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +472,8 @@ class AutocompleteUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tagsList.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
const currentTag = getCurrentPartialTag(this.target);
|
const currentTag = getCurrentPartialTag(this.target);
|
||||||
|
|
||||||
@@ -488,6 +491,7 @@ class AutocompleteUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
||||||
@@ -517,26 +521,29 @@ class AutocompleteUI {
|
|||||||
alias.className = 'autocomplete-plus-alias';
|
alias.className = 'autocomplete-plus-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `autocomplete-plus-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
category.title = categoryText; // Full category on hover
|
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
const tagCount = document.createElement('span');
|
const tagCount = document.createElement('span');
|
||||||
tagCount.className = `autocomplete-plus-tag-count`;
|
tagCount.className = `autocomplete-plus-tag-count`;
|
||||||
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
||||||
|
|
||||||
|
// Create tooltip with more info
|
||||||
|
let tooltipText = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
|
if (aliasText.length > 0) {
|
||||||
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
|
}
|
||||||
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
tagRow.appendChild(alias);
|
|
||||||
tagRow.appendChild(category);
|
if (!settingValues.hideAlias) {
|
||||||
|
tagRow.appendChild(alias);
|
||||||
|
}
|
||||||
|
|
||||||
tagRow.appendChild(tagCount);
|
tagRow.appendChild(tagCount);
|
||||||
this.tagsList.appendChild(tagRow);
|
this.tagsList.appendChild(tagRow);
|
||||||
}
|
}
|
||||||
@@ -574,21 +581,14 @@ class AutocompleteUI {
|
|||||||
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
||||||
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
||||||
|
|
||||||
const maxWidth = Math.min(rootRect.width, viewportWidth / 2);
|
|
||||||
const naturalHeight = rootRect.height;
|
const naturalHeight = rootRect.height;
|
||||||
|
|
||||||
//Horizontal Collision Detection and Adjustment
|
|
||||||
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
|
||||||
leftPosition = viewportWidth - maxWidth - margin.right;
|
|
||||||
}
|
|
||||||
if (leftPosition < margin.left) {
|
|
||||||
leftPosition = margin.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical Collision Detection and Adjustment
|
// Vertical Collision Detection and Adjustment
|
||||||
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
||||||
const availableSpaceAbove = caretTop - margin.top;
|
const availableSpaceAbove = caretTop - margin.top;
|
||||||
|
|
||||||
|
let needsVerticalScroll = false;
|
||||||
|
let maxHeight = rootRect.height;
|
||||||
|
|
||||||
if (naturalHeight <= availableSpaceBelow) {
|
if (naturalHeight <= availableSpaceBelow) {
|
||||||
// Fits perfectly below the caret
|
// Fits perfectly below the caret
|
||||||
@@ -600,14 +600,15 @@ class AutocompleteUI {
|
|||||||
topPosition = caretTop - naturalHeight - margin.top;
|
topPosition = caretTop - naturalHeight - margin.top;
|
||||||
} else {
|
} else {
|
||||||
// Doesn't fit perfectly either below or above, needs scrolling.
|
// Doesn't fit perfectly either below or above, needs scrolling.
|
||||||
|
needsVerticalScroll = true;
|
||||||
// Choose the position (above or below) that offers more space.
|
// Choose the position (above or below) that offers more space.
|
||||||
if (availableSpaceBelow >= availableSpaceAbove) {
|
if (availableSpaceBelow >= availableSpaceAbove) {
|
||||||
// Scroll below: topPosition remains as initially calculated
|
// Scroll below: topPosition remains as initially calculated
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceBelow}px`;
|
maxHeight = availableSpaceBelow;
|
||||||
} else {
|
} else {
|
||||||
// Scroll above: Position near the top edge and set max-height
|
// Scroll above: Position near the top edge and set max-height
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceAbove}px`;
|
maxHeight = availableSpaceAbove;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,16 +617,30 @@ class AutocompleteUI {
|
|||||||
if (topPosition < margin.top) {
|
if (topPosition < margin.top) {
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
// If pushed down, recalculate max-height if it was set based on top alignment
|
// If pushed down, recalculate max-height if it was set based on top alignment
|
||||||
if (this.tagsList.style.maxHeight && availableSpaceBelow < availableSpaceAbove) {
|
if (availableSpaceBelow < availableSpaceAbove) {
|
||||||
// Recalculate max-height based on space from the top margin
|
// Recalculate max-height based on space from the top margin
|
||||||
this.tagsList.style.maxHeight = `${viewportHeight - margin.top - margin.bottom}px`;
|
maxHeight = viewportHeight - margin.top - margin.bottom;
|
||||||
|
needsVerticalScroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate maxWidth considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = needsVerticalScroll ? getScrollbarWidth() : 0;
|
||||||
|
const maxWidth = Math.min(rootRect.width + scrollbarWidth, viewportWidth / 2);
|
||||||
|
|
||||||
|
// Horizontal Collision Detection and Adjustment
|
||||||
|
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
||||||
|
leftPosition = viewportWidth - maxWidth - margin.right;
|
||||||
|
}
|
||||||
|
if (leftPosition < margin.left) {
|
||||||
|
leftPosition = margin.left;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the calculated position and display the element
|
// Apply the calculated position and display the element
|
||||||
this.root.style.left = `${leftPosition}px`;
|
this.root.style.left = `${leftPosition}px`;
|
||||||
this.root.style.top = `${topPosition}px`;
|
this.root.style.top = `${topPosition}px`;
|
||||||
this.root.style.maxWidth = `${maxWidth}px`;
|
this.root.style.maxWidth = `${maxWidth}px`;
|
||||||
|
this.tagsList.style.maxHeight = `${maxHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Highlights the item (row) at the given index */
|
/** Highlights the item (row) at the given index */
|
||||||
|
|||||||
+26
-23
@@ -154,6 +154,7 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
}
|
}
|
||||||
const csvText = await response.text();
|
const csvText = await response.text();
|
||||||
const lines = csvText.split('\n').filter(line => line.trim().length > 0);
|
const lines = csvText.split('\n').filter(line => line.trim().length > 0);
|
||||||
|
const totalLines = lines.length;
|
||||||
|
|
||||||
const startIndex = lines[0].toLowerCase().startsWith(TAGS_CSV_HEADER) ? 1 : 0;
|
const startIndex = lines[0].toLowerCase().startsWith(TAGS_CSV_HEADER) ? 1 : 0;
|
||||||
|
|
||||||
@@ -183,8 +184,18 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
updateMaxTagLength(tag.length);
|
updateMaxTagLength(tag.length);
|
||||||
|
|
||||||
autoCompleteData[siteName].sortedTags.push(tagData);
|
autoCompleteData[siteName].sortedTags.push(tagData);
|
||||||
|
} else {
|
||||||
|
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set the tag and its alias in the maps
|
// Sort by count in descending order
|
||||||
|
autoCompleteData[siteName].sortedTags.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
// Build maps as before, but ensure not to overwrite if already processed from extra files
|
||||||
|
autoCompleteData[siteName].sortedTags.forEach(tagData => {
|
||||||
|
if (!autoCompleteData[siteName].tagMap.has(tagData.tag)) {
|
||||||
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
||||||
if (tagData.alias && Array.isArray(tagData.alias)) {
|
if (tagData.alias && Array.isArray(tagData.alias)) {
|
||||||
tagData.alias.forEach(alias => {
|
tagData.alias.forEach(alias => {
|
||||||
@@ -193,11 +204,8 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
||||||
@@ -268,17 +276,17 @@ async function loadCooccurrence(csvUrl, siteName) {
|
|||||||
|
|
||||||
const startIndex = lines[0].startsWith('tag_a,tag_b,count') ? 1 : 0;
|
const startIndex = lines[0].startsWith('tag_a,tag_b,count') ? 1 : 0;
|
||||||
|
|
||||||
await processCooccurrenceInChunks(lines, startIndex, autoCompleteData[siteName].cooccurrenceMap, csvUrl, siteName);
|
await processInChunks(lines, startIndex, autoCompleteData[siteName].cooccurrenceMap, csvUrl, siteName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Autocomplete-Plus] Failed to fetch or process cooccurrence data from ${csvUrl}:`, error);
|
console.error(`[Autocomplete-Plus] Failed to fetch or process cooccurrence data from ${csvUrl}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Co-Occurrence CSV data in chunks to avoid blocking the UI.
|
* Process CSV data in chunks to avoid blocking the UI.
|
||||||
* Modifies the targetMap directly.
|
* Modifies the targetMap directly.
|
||||||
*/
|
*/
|
||||||
function processCooccurrenceInChunks(lines, startIndex, targetMap, csvUrl, siteName) {
|
function processInChunks(lines, startIndex, targetMap, csvUrl, siteName) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const CHUNK_SIZE = 10000;
|
const CHUNK_SIZE = 10000;
|
||||||
let i = startIndex;
|
let i = startIndex;
|
||||||
@@ -289,7 +297,7 @@ function processCooccurrenceInChunks(lines, startIndex, targetMap, csvUrl, siteN
|
|||||||
|
|
||||||
for (; i < endIndex; i++) {
|
for (; i < endIndex; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const columns = line.split(",");
|
const columns = parseCSVLine(line);
|
||||||
|
|
||||||
if (columns.length >= 3) {
|
if (columns.length >= 3) {
|
||||||
const tagA = columns[0].trim();
|
const tagA = columns[0].trim();
|
||||||
@@ -299,19 +307,17 @@ function processCooccurrenceInChunks(lines, startIndex, targetMap, csvUrl, siteN
|
|||||||
if (!tagA || !tagB || isNaN(count)) continue;
|
if (!tagA || !tagB || isNaN(count)) continue;
|
||||||
|
|
||||||
// Add tagA -> tagB relationship
|
// Add tagA -> tagB relationship
|
||||||
let subMapA = targetMap.get(tagA);
|
if (!targetMap.has(tagA)) {
|
||||||
if (!subMapA) {
|
targetMap.set(tagA, new Map());
|
||||||
subMapA = new Map();
|
|
||||||
targetMap.set(tagA, subMapA);
|
|
||||||
}
|
}
|
||||||
subMapA.set(tagB, count);
|
targetMap.get(tagA).set(tagB, count);
|
||||||
|
|
||||||
let subMapB = targetMap.get(tagB);
|
|
||||||
if (!subMapB) {
|
// Add tagB -> tagA relationship (bidirectional)
|
||||||
subMapB = new Map();
|
if (!targetMap.has(tagB)) {
|
||||||
targetMap.set(tagB, subMapB);
|
targetMap.set(tagB, new Map());
|
||||||
}
|
}
|
||||||
subMapB.set(tagA, count);
|
targetMap.get(tagB).set(tagA, count);
|
||||||
|
|
||||||
pairCount++;
|
pairCount++;
|
||||||
}
|
}
|
||||||
@@ -444,9 +450,6 @@ async function initializeDataFromCSV(csvListData, source) {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
Promise.all(tagsLoadPromiseFactories.map(factory => factory()))
|
Promise.all(tagsLoadPromiseFactories.map(factory => factory()))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Sort by count in descending order
|
|
||||||
autoCompleteData[source].sortedTags.sort((a, b) => b.count - a.count);
|
|
||||||
|
|
||||||
// Build FlexSearch index after tags are loaded
|
// Build FlexSearch index after tags are loaded
|
||||||
return buildFlexSearchIndex(source);
|
return buildFlexSearchIndex(source);
|
||||||
})
|
})
|
||||||
@@ -543,7 +546,7 @@ async function loadLoras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all data sources in parallel.
|
* Load all data sources asynchronously.
|
||||||
*/
|
*/
|
||||||
export async function loadDataAsync() {
|
export async function loadDataAsync() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|||||||
+47
-33
@@ -190,40 +190,41 @@ async function addExtraSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extra setting for checking new CSV updates
|
// Add extra setting for checking new CSV updates
|
||||||
app.ui.settings.addSetting({
|
// Note: Temporarily comment out until changes to remote files can be detected
|
||||||
id: id + ".check_new_csv",
|
// app.ui.settings.addSetting({
|
||||||
defaultValue: null,
|
// id: id + ".check_new_csv",
|
||||||
name: "Check CSV updates",
|
// defaultValue: null,
|
||||||
category: [name, "Misc", "Check new CSV"],
|
// name: "Check CSV updates",
|
||||||
type: () => {
|
// category: [name, "Misc", "Check new CSV"],
|
||||||
const lastCheckSpan = $el("span", {
|
// type: () => {
|
||||||
textContent: lastCheckTimeText,
|
// const lastCheckSpan = $el("span", {
|
||||||
className: "text-sm text-gray-500",
|
// textContent: lastCheckTimeText,
|
||||||
style: {
|
// className: "text-sm text-gray-500",
|
||||||
marginRight: "16px"
|
// style: {
|
||||||
}
|
// marginRight: "16px"
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
const checkButton = $el("button", {
|
// const checkButton = $el("button", {
|
||||||
textContent: "Check now",
|
// textContent: "Check now",
|
||||||
className: "p-button p-component p-button-primary",
|
// className: "p-button p-component p-button-primary",
|
||||||
onclick: async () => {
|
// onclick: async () => {
|
||||||
await performUpdateCheck(checkButton, lastCheckSpan);
|
// await performUpdateCheck(checkButton, lastCheckSpan);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return $el("div", {
|
// return $el("div", {
|
||||||
className: "flex-row items-center gap-2",
|
// className: "flex-row items-center gap-2",
|
||||||
}, [
|
// }, [
|
||||||
$el("div", {
|
// $el("div", {
|
||||||
className: "p-component",
|
// className: "p-component",
|
||||||
}, [
|
// }, [
|
||||||
lastCheckSpan,
|
// lastCheckSpan,
|
||||||
checkButton,
|
// checkButton,
|
||||||
]),
|
// ]),
|
||||||
]);
|
// ]);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,7 +241,7 @@ app.registerExtension({
|
|||||||
let rootPath = import.meta.url.replace("js/main.js", "");
|
let rootPath = import.meta.url.replace("js/main.js", "");
|
||||||
loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete
|
loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete
|
||||||
|
|
||||||
loadDataAsync();
|
await loadDataAsync();
|
||||||
},
|
},
|
||||||
|
|
||||||
// One the Settings Screen, displays reverse order in same category
|
// One the Settings Screen, displays reverse order in same category
|
||||||
@@ -382,6 +383,19 @@ app.registerExtension({
|
|||||||
onChange: (newVal, oldVal) => {
|
onChange: (newVal, oldVal) => {
|
||||||
settingValues.enableRelatedTags = newVal;
|
settingValues.enableRelatedTags = newVal;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Display settings ---
|
||||||
|
{
|
||||||
|
id: id + ".display_hide_alias",
|
||||||
|
name: "Hide Alias",
|
||||||
|
tooltip: "Hide alias in the autocomplete and related tags display",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
category: [name, "Display", "Hide Alias"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.hideAlias = newVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
+33
-23
@@ -4,6 +4,7 @@ import {
|
|||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
findAllTagPositions,
|
findAllTagPositions,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
|
getScrollbarWidth,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
isLongText,
|
isLongText,
|
||||||
isValidTag,
|
isValidTag,
|
||||||
@@ -259,7 +260,7 @@ class RelatedTagsUI {
|
|||||||
this.headerControls.appendChild(this.pinBtn);
|
this.headerControls.appendChild(this.pinBtn);
|
||||||
|
|
||||||
this.header.appendChild(this.headerControls);
|
this.header.appendChild(this.headerControls);
|
||||||
|
|
||||||
this.root.appendChild(this.header);
|
this.root.appendChild(this.header);
|
||||||
|
|
||||||
// Create a tbody for the tags
|
// Create a tbody for the tags
|
||||||
@@ -419,8 +420,8 @@ class RelatedTagsUI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagText = this.currentTag;
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category];
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
// Update header text with current tag
|
// Update header text with current tag
|
||||||
this.headerText.innerHTML = ''; // Clear previous content
|
this.headerText.innerHTML = ''; // Clear previous content
|
||||||
@@ -428,15 +429,15 @@ class RelatedTagsUI {
|
|||||||
|
|
||||||
const tagName = document.createElement('span');
|
const tagName = document.createElement('span');
|
||||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}\nAlias: ${aliasText}`;
|
||||||
tagName.dataset.tagCategory = categoryText;
|
tagName.dataset.tagCategory = categoryText;
|
||||||
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||||
? `${tagSourceIconHtml} ${tagText}`
|
? `${tagSourceIconHtml} ${tagData.tag}`
|
||||||
: `${tagText} ${tagSourceIconHtml}`;
|
: `${tagData.tag} ${tagSourceIconHtml}`;
|
||||||
} else {
|
} else {
|
||||||
tagName.textContent += tagText;
|
tagName.textContent += tagData.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -447,8 +448,8 @@ class RelatedTagsUI {
|
|||||||
this.headerAlias.innerHTML = '';
|
this.headerAlias.innerHTML = '';
|
||||||
|
|
||||||
// Add alias if available
|
// Add alias if available
|
||||||
if (tagData.alias?.length > 0) {
|
if (aliasText.length > 0 && !settingValues.hideAlias) {
|
||||||
this.headerAlias.textContent = tagData.alias.join(', ');
|
this.headerAlias.textContent = aliasText;
|
||||||
this.headerAlias.style.display = 'block';
|
this.headerAlias.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +486,9 @@ class RelatedTagsUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle column class based on settings
|
||||||
|
this.tagsContainer.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
|
|
||||||
// Create tag rows
|
// Create tag rows
|
||||||
@@ -503,6 +507,7 @@ class RelatedTagsUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('related-tag-item', tagData.source);
|
tagRow.classList.add('related-tag-item', tagData.source);
|
||||||
@@ -524,33 +529,30 @@ class RelatedTagsUI {
|
|||||||
alias.className = 'related-tag-alias';
|
alias.className = 'related-tag-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `related-tag-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
|
|
||||||
// Similarity
|
// Similarity
|
||||||
const similarity = document.createElement('span');
|
const similarity = document.createElement('span');
|
||||||
similarity.className = 'related-tag-similarity';
|
similarity.className = 'related-tag-similarity';
|
||||||
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
||||||
|
|
||||||
// Create tooltip with more info
|
// Create tooltip with more info
|
||||||
let tooltipText = `Tag: ${tagData.tag}\nSimilarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}`;
|
let tooltipText = `Similarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
tooltipText += `\nAlias: ${tagData.alias.join(', ')}`;
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
}
|
}
|
||||||
tagRow.title = tooltipText;
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
// Add cells to row
|
// Add cells to row
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
tagRow.appendChild(alias);
|
|
||||||
tagRow.appendChild(category);
|
if (!settingValues.hideAlias) {
|
||||||
|
tagRow.appendChild(alias);
|
||||||
|
}
|
||||||
|
|
||||||
tagRow.appendChild(similarity);
|
tagRow.appendChild(similarity);
|
||||||
|
|
||||||
return tagRow;
|
return tagRow;
|
||||||
@@ -580,7 +582,7 @@ class RelatedTagsUI {
|
|||||||
|
|
||||||
const newHeaderRect = this.header.getBoundingClientRect();
|
const newHeaderRect = this.header.getBoundingClientRect();
|
||||||
|
|
||||||
if(this.relatedTags.length > 0){
|
if (this.relatedTags.length > 0) {
|
||||||
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,6 +671,10 @@ class RelatedTagsUI {
|
|||||||
area.y = targetRect.bottom;
|
area.y = targetRect.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(elemWidth + scrollbarWidth, maxWidth);
|
||||||
|
|
||||||
// Adjust x position to avoid overflow
|
// Adjust x position to avoid overflow
|
||||||
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
||||||
} else {
|
} else {
|
||||||
@@ -685,6 +691,10 @@ class RelatedTagsUI {
|
|||||||
area.x = targetRect.right;
|
area.x = targetRect.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(area.width + scrollbarWidth, viewportWidth - margin.left - margin.right);
|
||||||
|
|
||||||
// Adjust y position to avoid overflow
|
// Adjust y position to avoid overflow
|
||||||
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export const settingValues = {
|
|||||||
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
||||||
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
||||||
|
|
||||||
|
// Display settings
|
||||||
|
hideAlias: false, // Hide alias in the autocomplete and related tags display
|
||||||
|
|
||||||
|
|
||||||
// Internal logic settings
|
// Internal logic settings
|
||||||
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
||||||
|
|||||||
@@ -584,3 +584,38 @@ export function getViewportMargin() {
|
|||||||
right: rightBarRect.width,
|
right: rightBarRect.width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache for scrollbar width measurement
|
||||||
|
let _cachedScrollbarWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures and returns the scrollbar width for the current browser.
|
||||||
|
* Creates a temporary element to measure the scrollbar width accurately.
|
||||||
|
* @returns {number} The scrollbar width in pixels
|
||||||
|
*/
|
||||||
|
export function getScrollbarWidth() {
|
||||||
|
// Return cached value if available
|
||||||
|
if (_cachedScrollbarWidth !== undefined) {
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary div with scrollbar
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
outer.style.visibility = 'hidden';
|
||||||
|
outer.style.position = 'absolute';
|
||||||
|
outer.style.overflow = 'scroll';
|
||||||
|
outer.style.top = '-9999px';
|
||||||
|
outer.style.width = '100px';
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.style.width = '100%';
|
||||||
|
outer.appendChild(inner);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
_cachedScrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||||
|
|
||||||
|
document.body.removeChild(outer);
|
||||||
|
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user