actually works
Some checks failed
Gitea/kapitanbooru-uploader/pipeline/head There was a failure building this commit

Now with tagger

Miejsce na zdjęcie

Linki do wiki

Zapis ustawień

Tagger działa w tle

Kolorujemy pliki po ratingu

Tagger cache

Tagi w bazie

Pobranie implikacji tagów

Autocomplete

Podział na pliki i skrypty + nowe API

Structure for package

Version 0.1.0
This commit is contained in:
2025-02-13 22:11:35 +01:00
commit 5a97d610a7
18 changed files with 3069 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
import tkinter as tk
from tkinter import font
from .TagsRepo import TagsRepo
from .common import open_tag_wiki_url
from .tag_processing import process_tag
from .settings import Settings
class AutocompleteEntry(tk.Entry):
def __init__(self, master, tags_repo: TagsRepo, callback=None, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.tags_repo = tags_repo
self.callback = callback
self.listbox = None
self.listbox_window = None
self.suggestions = []
self.suggestion_map = {}
self.search_after_id = None # Przechowuje ID opóźnionego wyszukiwania
self.selection_index = -1
self.bind("<KeyRelease>", self.on_keyrelease)
self.bind("<Down>", self.on_down)
self.bind("<Up>", self.on_up)
self.bind("<Return>", self.on_return)
self.bind("<Tab>", self.on_return)
self.bind("<FocusOut>", lambda e: self.hide_listbox())
def on_keyrelease(self, event):
if event.keysym in ("Down", "Up", "Return", "Tab"):
return
if event.keysym == "Escape":
self.hide_listbox()
return
if self.search_after_id:
self.after_cancel(self.search_after_id)
self.search_after_id = self.after(200, self.update_suggestions)
def update_suggestions(self):
self.search_after_id = None
# Pobieramy cały tekst oraz indeks kursora
full_text = self.get()
# self.index(tk.INSERT) zwraca indeks w formacie "linia.kolumna", możemy go wykorzystać jako indeks znakowy
text_before_cursor = full_text[: self.index(tk.INSERT)]
# Jeżeli ostatni znak to spacja, to znaczy, że użytkownik zakończył ostatni tag nie sugerujemy
if text_before_cursor and text_before_cursor[-1].isspace():
self.hide_listbox()
return
# Podziel tekst przed kursorem na tokeny (oddzielone spacjami)
tokens = text_before_cursor.split()
prefix = tokens[-1] if tokens else ""
if not prefix:
self.hide_listbox()
return
# Pobieramy sugestie na podstawie prefixu
self.suggestions = self.get_suggestions(prefix)
if self.suggestions:
self.show_listbox()
else:
self.hide_listbox()
def on_return(self, event):
if self.listbox and self.selection_index >= 0:
selected_display = self.listbox.get(self.selection_index)
# Pobieramy wartość do wstawienia z mapy (czyli bez liczby postów)
suggestion = self.suggestion_map.get(selected_display, selected_display)
tag = suggestion
else:
tag = self.get().strip()
if tag and self.callback:
self.callback(tag)
self.delete(0, tk.END)
self.hide_listbox()
return "break"
def get_suggestions(self, prefix):
try:
conn = self.tags_repo.get_conn()
cursor = conn.cursor()
query = """
SELECT name, category, post_count FROM tags
WHERE name LIKE ? AND post_count >= 1
ORDER BY post_count DESC
LIMIT 10
"""
cursor.execute(query, (prefix + "%",))
results = cursor.fetchall()
conn.close()
# Mapowanie kategorii na prefiksy
prefix_map = {1: "artist:", 3: "copyright:", 4: "character:", 5: "meta:"}
suggestions = []
# Utwórz słownik mapujący tekst wyświetlany (z liczbą) na tekst do wstawienia (bez liczby)
self.suggestion_map = {}
for row in results:
name, category, post_count = row
tag_insert = prefix_map.get(category, "") + name
display_text = f"{tag_insert} ({post_count})"
suggestions.append(display_text)
self.suggestion_map[display_text] = tag_insert
return suggestions
except Exception as e:
print("Błąd przy pobieraniu sugestii:", e)
return []
def show_listbox(self):
if self.listbox_window:
self.listbox_window.destroy()
self.listbox_window = tk.Toplevel(self)
self.listbox_window.wm_overrideredirect(True)
self.listbox = tk.Listbox(self.listbox_window, height=6)
self.listbox.bind("<Button-1>", self.on_listbox_click)
self.listbox.bind("<Motion>", self.on_listbox_motion)
for suggestion in self.suggestions:
self.listbox.insert(tk.END, suggestion)
self.listbox.pack(fill=tk.BOTH, expand=True)
# Pobierz czcionkę używaną w listboxie
list_font = font.Font(font=self.listbox.cget("font"))
# Oblicz maksymalną szerokość na podstawie najdłuższego elementu
max_width = (
max(list_font.measure(item) for item in self.suggestions) + 20
) # +20 dla marginesu
# Ustaw szerokość okna na podstawie najszerszego elementu
self.listbox_window.geometry(f"{max_width}x200") # 200 - wysokość okna
# Umieszczamy okno poniżej pola autouzupełniania
x = self.winfo_rootx()
y = self.winfo_rooty() + self.winfo_height()
self.listbox_window.geometry("+%d+%d" % (x, y))
self.listbox_window.deiconify()
self.selection_index = -1
def hide_listbox(self):
if self.listbox_window:
self.listbox_window.destroy()
self.listbox_window = None
self.listbox = None
self.selection_index = -1
def on_listbox_click(self, event):
if self.listbox:
index = self.listbox.curselection()
if index:
value = self.listbox.get(index)
self.delete(0, tk.END)
self.insert(tk.END, value)
self.hide_listbox()
return "break"
def on_listbox_motion(self, event):
if self.listbox:
self.listbox.selection_clear(0, tk.END)
index = self.listbox.nearest(event.y)
self.listbox.selection_set(first=index)
self.selection_index = index
def on_down(self, event):
if self.listbox:
self.selection_index = (self.selection_index + 1) % self.listbox.size()
self.listbox.selection_clear(0, tk.END)
self.listbox.selection_set(self.selection_index)
self.listbox.activate(self.selection_index)
return "break"
def on_up(self, event):
if self.listbox:
self.selection_index = (self.selection_index - 1) % self.listbox.size()
self.listbox.selection_clear(0, tk.END)
self.listbox.selection_set(self.selection_index)
self.listbox.activate(self.selection_index)
return "break"
class TagManager(tk.Frame):
"""
This widget holds a tag input entry (with autocompletion) and a display area
that shows the entered tags. In the display area, left-clicking on a tag removes it,
and right-clicking opens its wiki URL. Tag appearance is adjusted (color/underline)
based on custom logic.
"""
def __init__(self, master, settings: Settings, tags_repo: TagsRepo, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.tags_repo = tags_repo
self.settings = settings
self.manual_tags = [] # List to hold manually entered tags
# Entry for new tags (with autocompletion)
self.entry = AutocompleteEntry(self, callback=self.add_tag, tags_repo=self.tags_repo)
self.entry.pack(fill=tk.X, padx=5, pady=5)
# Text widget for displaying already entered tags
self.tags_display = tk.Text(self, wrap="word", height=4)
self.tags_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tags_display.config(state="disabled")
# (Optional: add a scrollbar if needed)
def add_tag(self, tag):
"""Add a new tag if it is not already present."""
if tag and tag not in self.manual_tags:
self.manual_tags.append(tag)
self.update_tags_display()
def update_tags_display(self):
"""Refresh the text widget to display all manual tags with styling and event bindings."""
self.tags_display.config(state="normal")
self.tags_display.delete("1.0", tk.END)
for tag in self.manual_tags:
# Process tag to decide its style
_, deprecated = process_tag(tag, self.tags_repo)
if deprecated is True:
color = "red"
underline = 1
elif deprecated is None:
color = "darkorange"
underline = 1
else:
color = "blue"
underline = 0
start_index = self.tags_display.index(tk.INSERT)
self.tags_display.insert(tk.INSERT, tag)
end_index = self.tags_display.index(tk.INSERT)
tag_name = "manual_" + tag
self.tags_display.tag_add(tag_name, start_index, end_index)
self.tags_display.tag_configure(
tag_name, foreground=color, underline=underline
)
# Left-click: remove tag; Right-click: open wiki URL
self.tags_display.tag_bind(tag_name, "<Button-1>", self.remove_tag)
self.tags_display.tag_bind(tag_name, "<Button-3>", self.open_tag_wiki_url)
self.tags_display.insert(tk.INSERT, " ")
self.tags_display.config(state="disabled")
def remove_tag(self, event):
"""Remove the clicked tag from the list and update the display."""
index = self.tags_display.index("@%d,%d" % (event.x, event.y))
for t in self.tags_display.tag_names(index):
if t.startswith("manual_"):
tag = t[len("manual_") :]
if tag in self.manual_tags:
self.manual_tags.remove(tag)
self.update_tags_display()
break
def open_tag_wiki_url(self, event):
"""Open a wiki URL for the clicked tag."""
index = self.tags_display.index("@%d,%d" % (event.x, event.y))
for t in self.tags_display.tag_names(index):
if t.startswith("manual_"):
tag = t[len("manual_") :]
open_tag_wiki_url(tag, self.settings)
break