Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9273fb74d | ||
|
|
cf30c94faa | ||
|
|
828c08df0c | ||
|
|
3dbfd29274 | ||
|
|
86527e35b4 | ||
|
|
1bc609893b | ||
|
|
a536d58cf2 | ||
|
|
4c921da6cb | ||
|
|
98bf626ec1 | ||
|
|
57f80f0991 | ||
|
|
31f8fe4517 | ||
|
|
292c99ae17 | ||
|
|
86b63047c2 |
@@ -8,11 +8,6 @@
|
||||
|
||||
**ComfyUI-Autocomplete-Plus** is a custom node that provides multiple input assistance features for any text area in [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Currently, it supports Danbooru and e621 tags (e621 does not support some functions).
|
||||
|
||||
## Recent Updates :fire:
|
||||
- Added automatic and manual update check functionality for CSV files
|
||||
- Fixed bug where autocomplete suggestions wouldn't display at certain timings with Microsoft IME
|
||||
- Support for loading and displaying e621 tag CSV
|
||||
|
||||
## Features
|
||||
|
||||
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
||||
@@ -150,6 +145,33 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
||||
|
||||
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Disabling CSV Update Check on Startup
|
||||
|
||||
By default, ComfyUI performs CSV file update checks and downloads at regular intervals during startup.
|
||||
When starting in an environment without internet access, startup may be delayed until a timeout occurs.
|
||||
|
||||
You can skip the check process during ComfyUI startup by following these steps:
|
||||
|
||||
1. Start ComfyUI once with this custom node installed to generate the `csv_meta.json` file.
|
||||
The `csv_meta.json` file is created directly under this custom node's folder.
|
||||
2. Open `csv_meta.json` in a text editor and change the value of `check_updates_on_startup` from `true` to `false` and save.
|
||||
If `check_updates_on_startup` does not exist, add it under `version`.
|
||||
|
||||
**`csv_meta.json` after modification:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"check_updates_on_startup": false,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Additional notes:**
|
||||
- The check process will not be performed until the value of `check_updates_on_startup` is changed back to `true` or the `version` is switched.
|
||||
- Even when `check_updates_on_startup` is `false`, manual checking is still possible by pressing the `Check CSV updates` button in the Autocomplete Plus settings.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Performance
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from .modules.api import *
|
||||
from .modules.api import *
|
||||
from .modules import downloader
|
||||
|
||||
# check and download necessary csv files
|
||||
|
||||
+27
-5
@@ -6,11 +6,6 @@
|
||||
|
||||
**ComfyUI-Autocomplete-Plus** は、[ComfyUI](https://github.com/comfyanonymous/ComfyUI) の任意のテキストエリアに複数の入力支援機能を提供するカスタムノードです。現在はDanbooruとe621のタグに対応しています(e621は一部の機能が未対応です)。
|
||||
|
||||
## 最近の更新 :fire:
|
||||
- CSV ファイルの自動・手動更新チェック機能の追加
|
||||
- Microsoft IMEでオートコンプリート候補が表示されないタイミングがある不具合の修正
|
||||
- e621タグ CSV の読み込みと表示のサポート
|
||||
|
||||
## 特徴
|
||||
|
||||
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
||||
@@ -148,6 +143,33 @@ worst_quality,5,9999999,
|
||||
|
||||
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
||||
|
||||
## 上級者向け設定
|
||||
|
||||
### 起動時のCSV更新チェックを無効化する
|
||||
|
||||
デフォルトの動作では、ComfyUI起動時に一定の間隔で CSV ファイルの更新チェックとダウンロード行います。
|
||||
インターネットにアクセス出来ない環境で起動した場合、タイムアウトが発生するまで起動が遅延する事があります。
|
||||
|
||||
以下の手順を行う事により、ComfyUI起動時のチェック処理をスキップする事が出来ます。
|
||||
|
||||
1. このカスタムノードをインストールした状態でComfyUIを一度起動し、 `csv_meta.json` ファイルを生成する
|
||||
`csv_meta.json` はこのカスタムノードのフォルダー直下に作成されます
|
||||
2. `csv_meat.json` をテキストエディターで開き、`check_updates_on_startup` の値を `true` -> `false` に変更し保存する
|
||||
`check_updates_on_startup` が存在しない場合、 `version` の下に追記してください
|
||||
|
||||
**変更後の `csv_meta.json`:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"check_updates_on_startup": false,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**補足事項:**
|
||||
- `check_updates_on_startup` の値を再び `true` にするか、 `version` が切り替わるまでチェック処理は行われなくなります
|
||||
- `check_updates_on_startup` が `false` でも、Autocompelte Plusの設定から `Check CSV updates` のボタンを押す事で手動チェックが可能です
|
||||
|
||||
## 既知の問題
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
+85
-83
@@ -10,11 +10,12 @@ from . import downloader as dl
|
||||
# os.path.join(..., '..', 'data') goes up one level and then into 'data'
|
||||
DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "data"))
|
||||
|
||||
DANBOORU_PREFIX = 'danbooru'
|
||||
E621_PREFIX = 'e621'
|
||||
DANBOORU_PREFIX = "danbooru"
|
||||
E621_PREFIX = "e621"
|
||||
|
||||
TAGS_SUFFIX = "tags"
|
||||
COOCCURRENCE_SUFFIX = "tags_cooccurrence"
|
||||
|
||||
TAGS_SUFFIX = 'tags'
|
||||
COOCCURRENCE_SUFFIX = 'tags_cooccurrence'
|
||||
|
||||
def get_csv_file_status():
|
||||
"""
|
||||
@@ -23,17 +24,17 @@ def get_csv_file_status():
|
||||
|
||||
data = {
|
||||
DANBOORU_PREFIX: {
|
||||
'base_tags': False,
|
||||
'extra_tags': [],
|
||||
'base_cooccurrence': False,
|
||||
'extra_cooccurrence': [],
|
||||
"base_tags": False,
|
||||
"extra_tags": [],
|
||||
"base_cooccurrence": False,
|
||||
"extra_cooccurrence": [],
|
||||
},
|
||||
E621_PREFIX: {
|
||||
'base_tags': False,
|
||||
'extra_tags': [],
|
||||
'base_cooccurrence': False,
|
||||
'extra_cooccurrence': [],
|
||||
}
|
||||
"base_tags": False,
|
||||
"extra_tags": [],
|
||||
"base_cooccurrence": False,
|
||||
"extra_cooccurrence": [],
|
||||
},
|
||||
}
|
||||
|
||||
for prefix in [DANBOORU_PREFIX, E621_PREFIX]:
|
||||
@@ -46,27 +47,28 @@ def get_csv_file_status():
|
||||
tags_extra_files = []
|
||||
cooccurrence_extra_files = []
|
||||
|
||||
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith('.csv')]
|
||||
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith(".csv")]
|
||||
|
||||
# Create extra CSV files list
|
||||
for filename in all_csv_files:
|
||||
if filename in [base_tags_file, base_cooccurrence_file]:
|
||||
continue # Skip base files
|
||||
continue # Skip base files
|
||||
if COOCCURRENCE_SUFFIX in filename.lower():
|
||||
cooccurrence_extra_files.append(filename)
|
||||
elif TAGS_SUFFIX in filename.lower():
|
||||
tags_extra_files.append(filename)
|
||||
|
||||
data[prefix] = {
|
||||
'base_tags': tags_base_exists,
|
||||
'extra_tags': tags_extra_files,
|
||||
'base_cooccurrence': cooccurrence_base_exists,
|
||||
'extra_cooccurrence': cooccurrence_extra_files,
|
||||
"base_tags": tags_base_exists,
|
||||
"extra_tags": tags_extra_files,
|
||||
"base_cooccurrence": cooccurrence_base_exists,
|
||||
"extra_cooccurrence": cooccurrence_extra_files,
|
||||
}
|
||||
|
||||
# Return the lists of extra files
|
||||
return data
|
||||
|
||||
|
||||
def get_last_check_time_from_metadata():
|
||||
"""
|
||||
Helper function to get the last remote check timestamp from csv_meta.json.
|
||||
@@ -75,23 +77,25 @@ def get_last_check_time_from_metadata():
|
||||
try:
|
||||
if not os.path.exists(dl.CSV_META_FILE):
|
||||
return None
|
||||
|
||||
with open(dl.CSV_META_FILE, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(dl.CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
|
||||
datasets = metadata.get("hf_datasets", [])
|
||||
if datasets and len(datasets) > 0:
|
||||
return datasets[0].get("last_remote_check_timestamp")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# --- API Endpoints ---
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv")
|
||||
async def get_csv_list(_request):
|
||||
"""
|
||||
Returns CSV file status.
|
||||
@@ -102,43 +106,48 @@ async def get_csv_list(_request):
|
||||
|
||||
response = {
|
||||
DANBOORU_PREFIX: {
|
||||
'base_tags': csv_file_status[DANBOORU_PREFIX]['base_tags'],
|
||||
'extra_tags': csv_file_status[DANBOORU_PREFIX]['extra_tags'],
|
||||
'base_cooccurrence': csv_file_status[DANBOORU_PREFIX]['base_cooccurrence'],
|
||||
'extra_cooccurrence': csv_file_status[DANBOORU_PREFIX]['extra_cooccurrence'],
|
||||
"base_tags": csv_file_status[DANBOORU_PREFIX]["base_tags"],
|
||||
"extra_tags": csv_file_status[DANBOORU_PREFIX]["extra_tags"],
|
||||
"base_cooccurrence": csv_file_status[DANBOORU_PREFIX]["base_cooccurrence"],
|
||||
"extra_cooccurrence": csv_file_status[DANBOORU_PREFIX]["extra_cooccurrence"],
|
||||
},
|
||||
E621_PREFIX: {
|
||||
'base_tags': csv_file_status[E621_PREFIX]['base_tags'],
|
||||
'extra_tags': csv_file_status[E621_PREFIX]['extra_tags'],
|
||||
'base_cooccurrence': csv_file_status[E621_PREFIX]['base_cooccurrence'],
|
||||
'extra_cooccurrence': csv_file_status[E621_PREFIX]['extra_cooccurrence'],
|
||||
}
|
||||
"base_tags": csv_file_status[E621_PREFIX]["base_tags"],
|
||||
"extra_tags": csv_file_status[E621_PREFIX]["extra_tags"],
|
||||
"base_cooccurrence": csv_file_status[E621_PREFIX]["base_cooccurrence"],
|
||||
"extra_cooccurrence": csv_file_status[E621_PREFIX]["extra_cooccurrence"],
|
||||
},
|
||||
}
|
||||
|
||||
# Print csv file status to the console for debugging
|
||||
print(f"""[Autocomplete-Plus] CSV file status:
|
||||
* Danbooru -> base: {response[DANBOORU_PREFIX]['base_tags']}, extra: {", ".join(response[DANBOORU_PREFIX]['extra_tags'])}
|
||||
* E621 -> base: {response[E621_PREFIX]['base_tags']}, extra: {", ".join(response[E621_PREFIX]['extra_tags'])}""")
|
||||
* Danbooru -> base: {response[DANBOORU_PREFIX]["base_tags"]}, extra: [{", ".join(response[DANBOORU_PREFIX]["extra_tags"])}]
|
||||
* E621 -> base: {response[E621_PREFIX]["base_tags"]}, extra: [{", ".join(response[E621_PREFIX]["extra_tags"])}]""")
|
||||
|
||||
return web.json_response(response)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/base')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/base")
|
||||
async def get_base_tags_file(request):
|
||||
"""
|
||||
Returns the base tags CSV file.
|
||||
"""
|
||||
source = str(request.match_info['source'])
|
||||
suffix = str(request.match_info['suffix'])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
||||
source = str(request.match_info["source"])
|
||||
suffix = str(request.match_info["suffix"])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||
TAGS_SUFFIX,
|
||||
COOCCURRENCE_SUFFIX,
|
||||
]:
|
||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||
|
||||
|
||||
file_path = os.path.join(DATA_DIR, f"{source}_{suffix}.csv")
|
||||
if not os.path.exists(file_path):
|
||||
return web.json_response({"error": "Base tags file not found"}, status=404)
|
||||
|
||||
return web.FileResponse(file_path)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/extra/{index}')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/extra/{index}")
|
||||
async def get_extra_tags_file(request):
|
||||
"""
|
||||
Returns the extra tags CSV file at the specified index.
|
||||
@@ -146,16 +155,19 @@ async def get_extra_tags_file(request):
|
||||
try:
|
||||
csv_file_status = get_csv_file_status()
|
||||
|
||||
source = str(request.match_info['source'])
|
||||
suffix = str(request.match_info['suffix'])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
||||
source = str(request.match_info["source"])
|
||||
suffix = str(request.match_info["suffix"])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||
TAGS_SUFFIX,
|
||||
COOCCURRENCE_SUFFIX,
|
||||
]:
|
||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||
|
||||
index = int(request.match_info['index'])
|
||||
if index < 0 or index >= len(csv_file_status[source][f'extra_{suffix}']):
|
||||
|
||||
index = int(request.match_info["index"])
|
||||
if index < 0 or index >= len(csv_file_status[source][f"extra_{suffix}"]):
|
||||
return web.json_response({"error": "Invalid index"}, status=404)
|
||||
|
||||
file_path = os.path.join(DATA_DIR, csv_file_status[source][f'extra_{suffix}'][index])
|
||||
file_path = os.path.join(DATA_DIR, csv_file_status[source][f"extra_{suffix}"][index])
|
||||
if not os.path.exists(file_path):
|
||||
return web.json_response({"error": "Extra tags file not found"}, status=404)
|
||||
|
||||
@@ -164,7 +176,8 @@ async def get_extra_tags_file(request):
|
||||
except ValueError:
|
||||
return web.json_response({"error": "Invalid index format"}, status=400)
|
||||
|
||||
@server.PromptServer.instance.routes.post('/autocomplete-plus/csv/force-check-updates')
|
||||
|
||||
@server.PromptServer.instance.routes.post("/autocomplete-plus/csv/force-check-updates")
|
||||
async def force_check_csv_updates(request):
|
||||
"""
|
||||
Forces a check for CSV file updates from HuggingFace, ignoring cooldown.
|
||||
@@ -172,29 +185,29 @@ async def force_check_csv_updates(request):
|
||||
"""
|
||||
try:
|
||||
print("[Autocomplete-Plus] Starting forced check for CSV updates from HuggingFace...")
|
||||
|
||||
|
||||
downloader = dl.Downloader()
|
||||
downloader.run_check_and_download(force_check=True)
|
||||
|
||||
|
||||
print("[Autocomplete-Plus] Forced check completed successfully.")
|
||||
|
||||
|
||||
# Get the updated last check time
|
||||
last_check_time = get_last_check_time_from_metadata()
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": "Force check completed successfully",
|
||||
"last_check_time": last_check_time
|
||||
})
|
||||
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Force check completed successfully",
|
||||
"last_check_time": last_check_time,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Autocomplete-Plus] Error during forced check: {e}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/last-check-time')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/last-check-time")
|
||||
async def get_last_check_time(_request):
|
||||
"""
|
||||
Returns the last remote check timestamp from csv_meta.json.
|
||||
@@ -202,26 +215,15 @@ async def get_last_check_time(_request):
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(dl.CSV_META_FILE):
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"message": "csv_meta.json file not found"
|
||||
})
|
||||
|
||||
return web.json_response({"last_check_time": None, "message": "csv_meta.json file not found"})
|
||||
|
||||
last_check_time = get_last_check_time_from_metadata()
|
||||
|
||||
|
||||
if last_check_time is not None:
|
||||
return web.json_response({
|
||||
"last_check_time": last_check_time
|
||||
})
|
||||
return web.json_response({"last_check_time": last_check_time})
|
||||
else:
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"message": "No datasets found in metadata"
|
||||
})
|
||||
|
||||
return web.json_response({"last_check_time": None, "message": "No datasets found in metadata"})
|
||||
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
return web.json_response({"last_check_time": None, "error": str(e)}, status=500)
|
||||
|
||||
+54
-26
@@ -20,7 +20,8 @@ CSV_META_FILE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", C
|
||||
|
||||
DEFAULT_CSV_METADATA = {
|
||||
"version": 1,
|
||||
"hf_datasets" : [
|
||||
"check_updates_on_startup": True,
|
||||
"hf_datasets": [
|
||||
{
|
||||
"hf_dataset_id": "newtextdoc1111/danbooru-tag-csv",
|
||||
"last_remote_check_timestamp": None,
|
||||
@@ -34,12 +35,15 @@ DEFAULT_CSV_METADATA = {
|
||||
"file_name": "danbooru_tags_cooccurrence.csv",
|
||||
"last_download": None,
|
||||
"last_modified_on_hf": None,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
# --- HuggingFace Constants ---
|
||||
HUGGINGFACE_URL = "https://huggingface.co"
|
||||
|
||||
|
||||
# --- Helper Functions ---
|
||||
def get_file_path(file_name: str) -> str:
|
||||
@@ -82,12 +86,14 @@ class Downloader:
|
||||
return default_metadata
|
||||
|
||||
try:
|
||||
with open(CSV_META_FILE, 'r', encoding='utf-8') as f:
|
||||
with open(CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
if not isinstance(metadata, dict) or metadata.get("version") != DEFAULT_CSV_METADATA["version"]:
|
||||
print(f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
||||
f"found {metadata.get('version')}. Using default metadata.")
|
||||
print(
|
||||
f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
||||
f"found {metadata.get('version')}. Using default metadata."
|
||||
)
|
||||
return default_metadata
|
||||
else:
|
||||
self.csv_meta_file_exists_at_start = True
|
||||
@@ -101,7 +107,7 @@ class Downloader:
|
||||
"""Saves metadata to CSV_META_FILE."""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(CSV_META_FILE), exist_ok=True)
|
||||
with open(CSV_META_FILE, 'w', encoding='utf-8') as f:
|
||||
with open(CSV_META_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.metadata, f, indent=2)
|
||||
except IOError as e:
|
||||
print(f"[Autocomplete-Plus] Error saving metadata to {CSV_META_FILE}: {e}")
|
||||
@@ -111,12 +117,15 @@ class Downloader:
|
||||
Retrieves the Last-Modified header for a file on HuggingFace and returns it as an ISO 8601 string.
|
||||
hf_filename should be the full filename, e.g., "danbooru_tags.csv".
|
||||
"""
|
||||
url = f"https://huggingface.co/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
||||
url = f"{HUGGINGFACE_URL}/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
||||
try:
|
||||
req = urllib.request.Request(url, method='HEAD',
|
||||
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"})
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
last_modified_http = response.getheader('Last-Modified')
|
||||
last_modified_http = response.getheader("Last-Modified")
|
||||
if last_modified_http:
|
||||
dt_object = parsedate_to_datetime(last_modified_http)
|
||||
if dt_object.tzinfo is None or dt_object.tzinfo.utcoffset(dt_object) is None:
|
||||
@@ -141,7 +150,7 @@ class Downloader:
|
||||
"""
|
||||
Downloads a file synchronously with progress display to a temporary location.
|
||||
"""
|
||||
download_url = f"https://huggingface.co/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
||||
download_url = f"{HUGGINGFACE_URL}/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
||||
final_path = get_file_path(file_name)
|
||||
temp_path = get_temp_download_path(file_name)
|
||||
now_utc = datetime.now(timezone.utc).isoformat()
|
||||
@@ -155,18 +164,28 @@ class Downloader:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
req = urllib.request.Request(download_url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"})
|
||||
req = urllib.request.Request(
|
||||
download_url,
|
||||
headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"},
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
total_size_str = response.getheader("Content-Length")
|
||||
total_size = int(total_size_str) if total_size_str else None
|
||||
chunk_size = 8192
|
||||
|
||||
with open(temp_path, "wb") as f_out, \
|
||||
tqdm(total=total_size, unit='B', unit_scale=True, unit_divisor=1024,
|
||||
desc=f"[Autocomplete-Plus] Downloading {file_name}", leave=False,
|
||||
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]'
|
||||
) as pbar:
|
||||
with (
|
||||
open(temp_path, "wb") as f_out,
|
||||
tqdm(
|
||||
total=total_size,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
desc=f"[Autocomplete-Plus] Downloading {file_name}",
|
||||
leave=False,
|
||||
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
|
||||
) as pbar,
|
||||
):
|
||||
while True:
|
||||
chunk = response.read(chunk_size)
|
||||
if not chunk:
|
||||
@@ -197,12 +216,15 @@ class Downloader:
|
||||
if os.path.exists(final_path):
|
||||
try:
|
||||
file_size_at_final = os.path.getsize(final_path)
|
||||
if file_size_at_final == 0 or \
|
||||
(total_size and total_size > 0 and file_size_at_final < total_size) or \
|
||||
(not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size):
|
||||
if (
|
||||
file_size_at_final == 0
|
||||
or (total_size and total_size > 0 and file_size_at_final < total_size)
|
||||
or (not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size)
|
||||
):
|
||||
os.remove(final_path)
|
||||
print(
|
||||
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}")
|
||||
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}"
|
||||
)
|
||||
except OSError as rm_e:
|
||||
print(f"[Autocomplete-Plus] Error removing potentially corrupted file {final_path}: {rm_e}")
|
||||
|
||||
@@ -221,7 +243,8 @@ class Downloader:
|
||||
perform_hf_check = False
|
||||
except (ValueError, KeyError, TypeError):
|
||||
print(
|
||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check.")
|
||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check."
|
||||
)
|
||||
|
||||
if perform_hf_check:
|
||||
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
||||
@@ -246,7 +269,8 @@ class Downloader:
|
||||
dataset_meta["last_remote_check_timestamp"] = now_utc.isoformat()
|
||||
else:
|
||||
print(
|
||||
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later.")
|
||||
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later."
|
||||
)
|
||||
|
||||
def _download_csv_files_if_needed(self, dataset_meta: dict):
|
||||
"""Downloads CSV files if they are missing, outdated, or previously failed."""
|
||||
@@ -299,10 +323,14 @@ class Downloader:
|
||||
"""
|
||||
Orchestrates the process of checking for updates and downloading CSV files.
|
||||
This is the main entry point for the downloader logic.
|
||||
|
||||
|
||||
Args:
|
||||
force_check: If True, forces a check of HuggingFace regardless of the last check timestamp.
|
||||
"""
|
||||
# If check_updates_on_startup is False and force_check is not set, skip the check
|
||||
if not (self.metadata.get("check_updates_on_startup", True) or force_check):
|
||||
print('[Autocomplete-Plus] "check_updates_on_startup" is disabled. Skipping CSV update check and download.')
|
||||
return
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
+10
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-autocomplete-plus"
|
||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||
version = "1.0.2"
|
||||
version = "1.2.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["",]
|
||||
|
||||
@@ -9,7 +9,16 @@ dependencies = ["",]
|
||||
Repository = "https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.12.2",
|
||||
]
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "newtextdoc1111"
|
||||
DisplayName = "ComfyUI-Autocomplete-Plus"
|
||||
Icon = ""
|
||||
|
||||
[tool.ruff]
|
||||
# Allow lines to be as long as 120.
|
||||
line-length = 120
|
||||
|
||||
+144
-147
@@ -37,6 +37,62 @@ body.dark-theme {
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-list {
|
||||
display: grid;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||
@@ -52,62 +108,6 @@ body.dark-theme {
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item span {
|
||||
align-content: center;
|
||||
padding: 4px 8px;
|
||||
@@ -124,11 +124,7 @@ body.dark-theme {
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.selected span {
|
||||
background-color: var(
|
||||
--comfy-menu-bg-selected,
|
||||
var(--comfy-button-bg)
|
||||
);
|
||||
font-weight: bold;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item:hover span {
|
||||
@@ -172,26 +168,91 @@ body.dark-theme {
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#related-tags-header {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
color: var(--descrip-text);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.related-tags-header-text {
|
||||
flex-grow: 1;
|
||||
.related-tags-header-text-container {
|
||||
display:flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-text {
|
||||
padding: 2px 0;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name {
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name .autocomplete-plus-tag-icon-svg {
|
||||
@@ -200,17 +261,22 @@ body.dark-theme {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-alias {
|
||||
overflow: auto;
|
||||
max-height: 5em;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--descrip-text);
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.related-tags-header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.related-tags-layout-toggle {
|
||||
padding: 2px 6px;
|
||||
.related-tags-header-controls button {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
@@ -218,19 +284,8 @@ body.dark-theme {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.related-tags-layout-toggle:hover {
|
||||
background-color: var(--comfy-button-bg);
|
||||
}
|
||||
|
||||
.related-tags-pin-toggle {
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
.related-tags-header-controls button:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
#related-tags-list {
|
||||
@@ -248,62 +303,6 @@ body.dark-theme {
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.related-tag-item span {
|
||||
align-content: center;
|
||||
@@ -324,11 +323,7 @@ body.dark-theme {
|
||||
}
|
||||
|
||||
.related-tag-item.selected span {
|
||||
background-color: var(
|
||||
--comfy-menu-bg-selected,
|
||||
var(--comfy-button-bg)
|
||||
);
|
||||
font-weight: bold;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.related-tag-item .related-tag-name {
|
||||
@@ -352,9 +347,11 @@ body.dark-theme {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.related-tags-loading-message {
|
||||
.related-tags-message {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-style: italic;
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
+59
-17
@@ -67,7 +67,7 @@ function searchRelatedTags(tag) {
|
||||
|
||||
const tagSource = TagSource.Danbooru; // TODO: Leave the tag source as Danbooru until e621_tags_cooccurrence.csv is ready
|
||||
|
||||
if (!tag || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
||||
if (!tag || !autoCompleteData[tagSource].initialized || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -199,11 +199,20 @@ class RelatedTagsUI {
|
||||
this.header = document.createElement('div');
|
||||
this.header.id = 'related-tags-header';
|
||||
|
||||
this.headerTextContainer = document.createElement('div');
|
||||
this.headerTextContainer.className = 'related-tags-header-text-container';
|
||||
this.header.appendChild(this.headerTextContainer);
|
||||
|
||||
// Create header text div for the left side
|
||||
this.headerText = document.createElement('div');
|
||||
this.headerText.className = 'related-tags-header-text';
|
||||
this.headerText.className = 'related-tags-header-tag-text';
|
||||
this.headerText.textContent = 'Related Tags';
|
||||
this.header.appendChild(this.headerText);
|
||||
this.headerTextContainer.appendChild(this.headerText);
|
||||
|
||||
// Create header alias div for the 2nd line
|
||||
this.headerAlias = document.createElement('div');
|
||||
this.headerAlias.className = 'related-tags-header-tag-alias';
|
||||
this.headerTextContainer.appendChild(this.headerAlias);
|
||||
|
||||
// Create header controls for the right side
|
||||
this.headerControls = document.createElement('div');
|
||||
@@ -249,7 +258,7 @@ class RelatedTagsUI {
|
||||
this.headerControls.appendChild(this.pinBtn);
|
||||
|
||||
this.header.appendChild(this.headerControls);
|
||||
|
||||
|
||||
this.root.appendChild(this.header);
|
||||
|
||||
// Create a tbody for the tags
|
||||
@@ -391,7 +400,7 @@ class RelatedTagsUI {
|
||||
*/
|
||||
#updateHeader() {
|
||||
// Find the tag data for the current tag
|
||||
const tagData = Object.values(TagSource)
|
||||
let tagData = Object.values(TagSource)
|
||||
.map((source) => {
|
||||
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
||||
return autoCompleteData[source].tagMap.get(this.currentTag);
|
||||
@@ -399,21 +408,49 @@ class RelatedTagsUI {
|
||||
})
|
||||
.find((tagData) => tagData !== undefined);
|
||||
|
||||
if (!tagData) {
|
||||
tagData = new TagData({
|
||||
tag: this.currentTag,
|
||||
source: TagSource.Danbooru,
|
||||
category: 'unknown',
|
||||
count: 0,
|
||||
alias: [],
|
||||
});
|
||||
}
|
||||
|
||||
const tagText = this.currentTag;
|
||||
const categoryText = TagCategory[tagData.source][tagData.category];
|
||||
|
||||
// Update header text with current tag
|
||||
this.headerText.innerHTML = ''; // Clear previous content
|
||||
this.headerText.textContent = 'Tags related to: ';
|
||||
|
||||
const tagName = document.createElement('span');
|
||||
tagName.className = 'related-tags-header-tag-name';
|
||||
tagName.textContent = this.currentTag;
|
||||
if (tagData && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||
tagName.dataset.tagCategory = categoryText;
|
||||
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>`;
|
||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||
? `${tagSourceIconHtml} ${tagData.tag}`
|
||||
: `${tagData.tag} ${tagSourceIconHtml}`;
|
||||
? `${tagSourceIconHtml} ${tagText}`
|
||||
: `${tagText} ${tagSourceIconHtml}`;
|
||||
} else {
|
||||
tagName.textContent += tagText;
|
||||
}
|
||||
|
||||
|
||||
this.headerText.appendChild(tagName);
|
||||
|
||||
// Clear previous alias
|
||||
this.headerAlias.style.display = 'none';
|
||||
this.headerAlias.innerHTML = '';
|
||||
|
||||
// Add alias if available
|
||||
if (tagData.alias?.length > 0) {
|
||||
this.headerAlias.textContent = tagData.alias.join(', ');
|
||||
this.headerAlias.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update pin button
|
||||
this.pinBtn.textContent = this.isPinned ? '🎯' : '📌';
|
||||
|
||||
@@ -432,7 +469,7 @@ class RelatedTagsUI {
|
||||
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
||||
// Show loading message
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'related-tags-loading-message';
|
||||
messageDiv.className = 'related-tags-message';
|
||||
messageDiv.textContent = `Initializing cooccurrence data... [${autoCompleteData[TagSource.Danbooru].baseLoadingProgress.cooccurrence}%]`;
|
||||
this.tagsContainer.appendChild(messageDiv);
|
||||
return;
|
||||
@@ -441,6 +478,7 @@ class RelatedTagsUI {
|
||||
if (!this.relatedTags || this.relatedTags.length === 0) {
|
||||
// Show no related tags message
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'related-tags-message';
|
||||
messageDiv.textContent = 'No related tags found';
|
||||
this.tagsContainer.appendChild(messageDiv);
|
||||
return;
|
||||
@@ -530,20 +568,24 @@ class RelatedTagsUI {
|
||||
this.root.style.maxWidth = '';
|
||||
this.tagsContainer.style.maxHeight = '';
|
||||
const rootRect = this.root.getBoundingClientRect();
|
||||
const headerRect = this.header.getBoundingClientRect();
|
||||
// Hide it again after measurement
|
||||
this.root.style.display = 'none';
|
||||
this.root.style.visibility = 'visible';
|
||||
|
||||
// Get the optimal placement area
|
||||
const placementArea = this.#getOptimalPlacementArea(rootRect.width, rootRect.height);
|
||||
|
||||
// Apply Styles
|
||||
// Apply position and size
|
||||
this.root.style.left = `${placementArea.x}px`;
|
||||
this.root.style.top = `${placementArea.y}px`;
|
||||
this.root.style.maxWidth = `${placementArea.width}px`;
|
||||
|
||||
this.tagsContainer.style.maxHeight = `${placementArea.height - headerRect.height}px`;
|
||||
const newHeaderRect = this.header.getBoundingClientRect();
|
||||
|
||||
if(this.relatedTags.length > 0){
|
||||
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
||||
}
|
||||
|
||||
// Hide it again after measurement
|
||||
this.root.style.display = 'none';
|
||||
this.root.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
/** Highlights the item (row) at the given index */
|
||||
|
||||
Reference in New Issue
Block a user