Better UI, edit tags
All checks were successful
Gitea/kapitanbooru-uploader/pipeline/head This commit looks good

This commit is contained in:
Michał Leśniak 2025-02-26 22:41:56 +01:00
parent dbea14888e
commit 5571e18102
3 changed files with 319 additions and 111 deletions

View File

@ -1,64 +1,96 @@
from .common import open_tag_wiki_url, open_webbrowser
from .ProgressFile import ProgressFile
from .autocomplete import TagManager
from .settings import Settings
from .tag_processing import TAG_FIXES, parse_parameters, process_tag
from .tagger_cache import TaggerCache
from .TagsRepo import TagsRepo
import networkx as nx
import requests
import wdtagger as wdt
from PIL import Image, ImageTk, PngImagePlugin
import glob import glob
import hashlib import hashlib
import inspect
import os import os
import queue
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from typing import Tuple from typing import Tuple
import networkx as nx
import requests
from PIL import Image, ImageTk, PngImagePlugin
import wdtagger as wdt
from .ProgressFile import ProgressFile
from .TagsRepo import TagsRepo
from .autocomplete import TagManager
from .common import get_auth_token, login, open_tag_wiki_url, open_webbrowser
from .settings import Settings
from .tag_processing import TAG_FIXES, parse_parameters, process_tag
from .tagger_cache import TaggerCache
class ProcessingDialog: class ProcessingDialog:
def __init__(self, root, target_function, *args): def __init__(self, root, target_function, *args):
self.root = root self.root = root
self.top = tk.Toplevel(root) # Create a top-level window self.top = tk.Toplevel(root)
self.top.title("Processing...") self.top.title("Processing...")
self.top.geometry("300x150") self.top.geometry("300x150")
self.top.protocol("WM_DELETE_WINDOW", self.on_close) # Handle close event self.top.protocol("WM_DELETE_WINDOW", self.on_close)
# Create Label and Progress Bar (or rotating animation)
self.label = tk.Label(self.top, text="Processing, please wait...") self.label = tk.Label(self.top, text="Processing, please wait...")
self.label.pack(pady=10) self.label.pack(pady=10)
# Start with indeterminate progress bar
self.progress = ttk.Progressbar(self.top, mode="indeterminate") self.progress = ttk.Progressbar(self.top, mode="indeterminate")
self.progress.pack(pady=10, fill="x") self.progress.pack(pady=10, fill="x")
self.progress.start(10) # Start animation self.progress.start(10)
# Create a thread for the target function # Setup communication queue and periodic checker
self.queue = queue.Queue()
self.running = True self.running = True
self.thread = threading.Thread( self.thread = threading.Thread(
target=self.run_task, args=(target_function, *args) target=self.run_task, args=(target_function, *args)
) )
self.thread.start() self.thread.start()
self.top.after(100, self.process_queue)
def process_queue(self):
"""Process messages from the background thread"""
while self.running:
try:
msg = self.queue.get_nowait()
if msg[0] == "mode":
self.progress.config(mode=msg[1])
if msg[1] == "determinate":
self.progress["value"] = 0
elif msg[0] == "max":
self.progress["maximum"] = msg[1]
elif msg[0] == "progress":
self.progress["value"] = msg[1]
elif msg[0] == "label":
self.label.config(text=msg[1])
self.top.update_idletasks()
except queue.Empty:
break
if self.running:
self.top.after(100, self.process_queue)
def run_task(self, target_function, *args): def run_task(self, target_function, *args):
"""Execute target function with progress queue if supported"""
try: try:
target_function(*args) # Run the function sig = inspect.signature(target_function)
if "progress_queue" in sig.parameters:
target_function(*args, progress_queue=self.queue)
else:
target_function(*args)
finally: finally:
self.close_dialog() # Close when done self.close_dialog()
def close_dialog(self): def close_dialog(self):
"""Safely close the dialog.""" """Safely close the dialog"""
if self.running: if self.running:
self.running = False self.running = False
self.top.after(0, self.top.destroy) # Ensure UI updates on the main thread self.progress.stop()
self.top.after(0, self.top.destroy)
def on_close(self): def on_close(self):
"""User manually closed the dialog, terminate thread.""" """Handle manual window closure"""
self.running = False self.running = False
self.top.destroy() self.top.destroy()
@ -271,12 +303,42 @@ class ImageBrowser(tk.Tk):
def create_menu(self): def create_menu(self):
menubar = tk.Menu(self) menubar = tk.Menu(self)
self.config(menu=menubar) self.config(menu=menubar)
menubar.add_command(label="Ustawienia", command=self.open_settings)
menubar.add_command(label="Wyczyść cache Taggera", command=self.clear_cache) # Create file menu and store it as instance variable
menubar.add_command(label="Wrzuć wszystko", command=self.upload_all_files) self.file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_command(
# File menu items - create references for items we need to control
self.file_menu.add_command(label="Otwórz folder", command=self.select_folder)
self.file_menu.add_separator()
self.file_menu.add_command(
label="Wyślij", command=self.upload_current_image, state=tk.DISABLED
)
self.file_menu.add_command(
label="Wyślij wszystko", command=self.upload_all_files, state=tk.DISABLED
)
self.file_menu.add_separator()
self.file_menu.add_command(
label="Podmień tagi", command=self.edit_current_image, state=tk.DISABLED
)
self.file_menu.add_command(
label="Otwórz post", command=self.view_current_post, state=tk.DISABLED
)
self.file_menu.add_separator()
self.file_menu.add_command(label="Zakończ", command=self.quit)
menubar.add_cascade(label="Plik", menu=self.file_menu)
# Options menu
options_menu = tk.Menu(menubar, tearoff=0)
options_menu.add_command(label="Ustawienia", command=self.open_settings)
options_menu.add_separator()
options_menu.add_command(
label="Wyczyść cache Taggera", command=self.clear_cache
)
options_menu.add_command(
label="Zregeneruj bazę tagów", command=self.regenerate_tags_db label="Zregeneruj bazę tagów", command=self.regenerate_tags_db
) )
menubar.add_cascade(label="Opcje", menu=options_menu)
def regenerate_tags_db(self): def regenerate_tags_db(self):
self.processing_dialog = ProcessingDialog(self, self.tags_repo.regenerate_db) self.processing_dialog = ProcessingDialog(self, self.tags_repo.regenerate_db)
@ -362,7 +424,7 @@ class ImageBrowser(tk.Tk):
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Lewa kolumna lista plików # Lewa kolumna lista plików
left_frame = tk.Frame(main_frame) left_frame = tk.Frame(main_frame)
left_frame.grid(row=0, column=0, sticky="ns") left_frame.grid(row=0, column=0, sticky=tk.NS)
self.listbox = tk.Listbox(left_frame, width=30) self.listbox = tk.Listbox(left_frame, width=30)
self.listbox.pack(side=tk.LEFT, fill=tk.Y) self.listbox.pack(side=tk.LEFT, fill=tk.Y)
self.listbox.bind("<<ListboxSelect>>", self.on_listbox_select) self.listbox.bind("<<ListboxSelect>>", self.on_listbox_select)
@ -374,7 +436,7 @@ class ImageBrowser(tk.Tk):
# Środkowa kolumna podgląd obrazu # Środkowa kolumna podgląd obrazu
center_frame = tk.Frame(main_frame) center_frame = tk.Frame(main_frame)
center_frame.grid(row=0, column=1, sticky="nsew", padx=10) center_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=10)
main_frame.grid_columnconfigure(1, weight=1) main_frame.grid_columnconfigure(1, weight=1)
main_frame.grid_rowconfigure(0, weight=1) main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_rowconfigure(1, weight=0) main_frame.grid_rowconfigure(1, weight=0)
@ -384,7 +446,7 @@ class ImageBrowser(tk.Tk):
# Prawa kolumna panel tagów i uploadu (ograniczona szerokość) # Prawa kolumna panel tagów i uploadu (ograniczona szerokość)
right_frame = tk.Frame(main_frame, width=300) right_frame = tk.Frame(main_frame, width=300)
right_frame.grid(row=0, column=2, sticky="nsew", padx=5) right_frame.grid(row=0, column=2, sticky=tk.NSEW, padx=5)
right_frame.grid_propagate(False) right_frame.grid_propagate(False)
right_frame.grid_columnconfigure(0, weight=1) right_frame.grid_columnconfigure(0, weight=1)
# Ustal wiersze: # Ustal wiersze:
@ -396,33 +458,33 @@ class ImageBrowser(tk.Tk):
# PNG Tags widget Text z scrollbar # PNG Tags widget Text z scrollbar
png_frame = tk.LabelFrame(right_frame, text="PNG Tags") png_frame = tk.LabelFrame(right_frame, text="PNG Tags")
png_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) png_frame.grid(row=0, column=0, sticky=tk.EW, padx=5, pady=5)
png_frame.grid_columnconfigure(0, weight=1) png_frame.grid_columnconfigure(0, weight=1)
self.png_tags_text = tk.Text(png_frame, wrap="word") self.png_tags_text = tk.Text(png_frame, wrap=tk.WORD)
self.png_tags_text.grid(row=0, column=0, sticky="ew") self.png_tags_text.grid(row=0, column=0, sticky=tk.EW)
scrollbar_png = tk.Scrollbar(png_frame, command=self.png_tags_text.yview) scrollbar_png = tk.Scrollbar(png_frame, command=self.png_tags_text.yview)
scrollbar_png.grid(row=0, column=1, sticky="ns") scrollbar_png.grid(row=0, column=1, sticky=tk.NS)
self.png_tags_text.config( self.png_tags_text.config(
yscrollcommand=scrollbar_png.set, state="disabled", height=4 yscrollcommand=scrollbar_png.set, state=tk.DISABLED, height=4
) )
# Tagger Tags widget Text z scrollbar # Tagger Tags widget Text z scrollbar
tagger_frame = tk.LabelFrame(right_frame, text="Tagger Tags") tagger_frame = tk.LabelFrame(right_frame, text="Tagger Tags")
tagger_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5) tagger_frame.grid(row=1, column=0, sticky=tk.EW, padx=5, pady=5)
tagger_frame.grid_columnconfigure(0, weight=1) tagger_frame.grid_columnconfigure(0, weight=1)
self.tagger_tags_text = tk.Text(tagger_frame, wrap="word") self.tagger_tags_text = tk.Text(tagger_frame, wrap=tk.WORD)
self.tagger_tags_text.grid(row=0, column=0, sticky="ew") self.tagger_tags_text.grid(row=0, column=0, sticky=tk.EW)
scrollbar_tagger = tk.Scrollbar( scrollbar_tagger = tk.Scrollbar(
tagger_frame, command=self.tagger_tags_text.yview tagger_frame, command=self.tagger_tags_text.yview
) )
scrollbar_tagger.grid(row=0, column=1, sticky="ns") scrollbar_tagger.grid(row=0, column=1, sticky=tk.NS)
self.tagger_tags_text.config( self.tagger_tags_text.config(
yscrollcommand=scrollbar_tagger.set, state="disabled", height=4 yscrollcommand=scrollbar_tagger.set, state=tk.DISABLED, height=4
) )
# Manual Tags Entry (stała wysokość) # Manual Tags Entry (stała wysokość)
manual_frame = tk.LabelFrame(right_frame, text="Manual Tags") manual_frame = tk.LabelFrame(right_frame, text="Manual Tags")
manual_frame.grid(row=2, column=0, sticky="nsew", padx=5, pady=5) manual_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=5, pady=5)
self.manual_tags_manager = TagManager( self.manual_tags_manager = TagManager(
manual_frame, self.settings, self.tags_repo manual_frame, self.settings, self.tags_repo
) )
@ -436,18 +498,18 @@ class ImageBrowser(tk.Tk):
# Final Tags widget Text z scrollbar, który rozszerza się # Final Tags widget Text z scrollbar, który rozszerza się
final_frame = tk.LabelFrame(right_frame, text="Final Tags") final_frame = tk.LabelFrame(right_frame, text="Final Tags")
final_frame.grid(row=3, column=0, sticky="nsew", padx=5, pady=5) final_frame.grid(row=3, column=0, sticky=tk.NSEW, padx=5, pady=5)
final_frame.grid_rowconfigure(0, weight=1) final_frame.grid_rowconfigure(0, weight=1)
final_frame.grid_columnconfigure(0, weight=1) final_frame.grid_columnconfigure(0, weight=1)
self.final_tags_text = tk.Text(final_frame, state="disabled", wrap="word") self.final_tags_text = tk.Text(final_frame, state=tk.DISABLED, wrap=tk.WORD)
self.final_tags_text.grid(row=0, column=0, sticky="nsew") self.final_tags_text.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar_final = tk.Scrollbar(final_frame, command=self.final_tags_text.yview) scrollbar_final = tk.Scrollbar(final_frame, command=self.final_tags_text.yview)
scrollbar_final.grid(row=0, column=1, sticky="ns") scrollbar_final.grid(row=0, column=1, sticky=tk.NS)
self.final_tags_text.config(yscrollcommand=scrollbar_final.set) self.final_tags_text.config(yscrollcommand=scrollbar_final.set)
# Panel uploadu i rating nie zmienia rozmiaru pionowo # Panel uploadu i rating nie zmienia rozmiaru pionowo
upload_frame = tk.Frame(right_frame) upload_frame = tk.Frame(right_frame)
upload_frame.grid(row=4, column=0, sticky="ew", padx=5, pady=5) upload_frame.grid(row=4, column=0, sticky=tk.EW, padx=5, pady=5)
self.rating_var = tk.StringVar(value="Unrated") self.rating_var = tk.StringVar(value="Unrated")
rating_options = ["General", "Sensitive", "Questionable", "Explicit", "Unrated"] rating_options = ["General", "Sensitive", "Questionable", "Explicit", "Unrated"]
self.rating_dropdown = tk.OptionMenu( self.rating_dropdown = tk.OptionMenu(
@ -458,21 +520,18 @@ class ImageBrowser(tk.Tk):
upload_frame, text="Upload", command=self.upload_current_image upload_frame, text="Upload", command=self.upload_current_image
) )
self.upload_button.pack(side=tk.LEFT, padx=5) self.upload_button.pack(side=tk.LEFT, padx=5)
self.progress_var = tk.IntVar(value=0) self.upload_button.config(state=tk.DISABLED)
self.progress_bar = ttk.Progressbar( self.view_post_button = tk.Button(
upload_frame, upload_frame, text="Wyświetl", command=self.view_current_post
orient="horizontal",
mode="determinate",
variable=self.progress_var,
maximum=100,
) )
self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) self.view_post_button.pack(side=tk.LEFT, padx=5)
self.view_post_button.config(state=tk.DISABLED)
# Na końcu okna głównego dodaj status bar: # Na końcu okna głównego dodaj status bar:
self.status_label = tk.Label( self.status_label = tk.Label(
main_frame, text="", bd=1, relief=tk.SUNKEN, anchor=tk.W main_frame, text="", bd=1, relief=tk.SUNKEN, anchor=tk.W
) )
self.status_label.grid(row=1, column=0, columnspan=3, sticky="ew") self.status_label.grid(row=1, column=0, columnspan=3, sticky=tk.EW)
def update_status_bar(self): def update_status_bar(self):
status_text = ( status_text = (
@ -585,31 +644,75 @@ class ImageBrowser(tk.Tk):
) )
# Jeśli aktualnie wybrany plik, zmień przycisk # Jeśli aktualnie wybrany plik, zmień przycisk
if self.current_index == idx: if self.current_index == idx:
self.after(0, self.set_upload_button_to_view_or_upload) self.after(0, self.update_button_states)
else: else:
self.uploaded[file_path] = False self.uploaded[file_path] = False
self.after(0, self.update_status_bar) self.after(0, self.update_status_bar)
except Exception as e: except Exception as e:
print("Błąd podczas sprawdzania paczki uploadu:", e) print("Błąd podczas sprawdzania paczki uploadu:", e)
def set_upload_button_to_view_or_upload(self): def update_button_states(self):
""" """
Ustawia przycisk uploadu na "Wyświetl" lub "Upload" w zależności od stan Update the state of UI elements based on current application state.
uploadu dla aktualnie wybranego pliku.
Synchronizes the enabled/disabled states of menu items and buttons with:
- Whether an image is currently selected (current_index exists)
- Whether the selected image has an existing post (post_id exists in uploaded)
State transitions:
- When no image is selected (current_index is None):
* Disables all upload-related buttons and menu items
- When an image is selected:
* Always enables 'Wyślij wszystko' (Send All)
* Enables 'Wyślij' (Send) if no post exists for the image
* Enables 'Podmień tagi' (Replace Tags) and 'Otwórz post' (Open Post)
if a post exists for the image
* Updates the upload button's text and command based on post existence
Modifies:
- File menu items: 'Wyślij', 'Wyślij wszystko', 'Podmień tagi', 'Otwórz post'
- Buttons: upload_button, view_post_button
Dependencies:
- Relies on self.current_index to determine selection state
- Checks self.uploaded against self.image_files for post existence
""" """
has_current = self.current_index is not None
post_id = (
self.uploaded.get(self.image_files[self.current_index])
if has_current
else None
)
# Common state for all elements
send_all_state = tk.NORMAL if has_current else tk.DISABLED
post_ops_state = tk.NORMAL if post_id else tk.DISABLED
# Update menu items
self.file_menu.entryconfig("Wyślij wszystko", state=send_all_state)
self.file_menu.entryconfig("Podmień tagi", state=post_ops_state)
self.file_menu.entryconfig("Otwórz post", state=post_ops_state)
# Update buttons
self.upload_button.config(
state=tk.NORMAL if has_current else tk.DISABLED,
text="Podmień tagi" if post_id else "Wyślij",
command=self.edit_current_image if post_id else self.upload_current_image,
)
self.view_post_button.config(state=post_ops_state)
# Special case for "Wyślij" menu item
wyślij_state = tk.DISABLED if post_id else tk.NORMAL
self.file_menu.entryconfig(
"Wyślij", state=wyślij_state if has_current else tk.DISABLED
)
def view_current_post(self):
"""Otwiera w przeglądarce URL posta."""
if self.current_index is None: if self.current_index is None:
return return
post_id = self.uploaded.get(self.image_files[self.current_index]) post_id = self.uploaded.get(self.image_files[self.current_index])
if post_id: if post_id:
self.upload_button.config(
text="Wyświetl", command=lambda: self.view_post(post_id)
)
else:
self.upload_button.config(text="Upload", command=self.upload_current_image)
def view_post(self, post_id):
"""Otwiera w przeglądarce URL posta."""
url = self.settings.base_url.rstrip("/") + "/post/view/" + str(post_id) url = self.settings.base_url.rstrip("/") + "/post/view/" + str(post_id)
open_webbrowser(url, self.settings) open_webbrowser(url, self.settings)
@ -635,7 +738,7 @@ class ImageBrowser(tk.Tk):
index = self.listbox.curselection()[0] index = self.listbox.curselection()[0]
self.current_index = index self.current_index = index
self.show_image(index) self.show_image(index)
self.set_upload_button_to_view_or_upload() self.update_button_states()
def upload_current_image(self): def upload_current_image(self):
""" """
@ -646,13 +749,10 @@ class ImageBrowser(tk.Tk):
file_path = self.image_files[self.current_index] file_path = self.image_files[self.current_index]
if self.uploaded.get(file_path, False): if self.uploaded.get(file_path, False):
# Jeśli plik już uploadowany, ustaw przycisk na "Wyświetl" # Jeśli plik już uploadowany, ustaw przycisk na "Wyświetl"
self.set_upload_button_to_view_or_upload() self.update_button_states()
return return
self.upload_button.config(state="disabled") self.upload_button.config(state=tk.DISABLED)
self.progress_var.set(0) self.processing_dialog = ProcessingDialog(self, self.upload_file, file_path)
threading.Thread(
target=self.upload_file, args=(file_path,), daemon=True
).start()
def show_image(self, index): def show_image(self, index):
""" """
@ -678,6 +778,18 @@ class ImageBrowser(tk.Tk):
except Exception as e: except Exception as e:
messagebox.showerror("Błąd", f"Nie można załadować obrazka:\n{e}") messagebox.showerror("Błąd", f"Nie można załadować obrazka:\n{e}")
def edit_current_image(self):
"""
Modyfikuje obrazek na serwerze.
Wysyła POST z md5 obrazka, tagami i ratingiem.
"""
file_path = self.image_files[self.current_index]
if not self.uploaded.get(file_path, False):
self.update_button_states()
return
self.upload_button.config(state=tk.DISABLED)
self.processing_dialog = ProcessingDialog(self, self.edit_file, file_path)
def update_display_image(self): def update_display_image(self):
""" """
Aktualizuje obrazek na podstawie aktualnego rozmiaru okna. Aktualizuje obrazek na podstawie aktualnego rozmiaru okna.
@ -756,7 +868,7 @@ class ImageBrowser(tk.Tk):
self.listbox.select_set(new_index) self.listbox.select_set(new_index)
self.listbox.activate(new_index) self.listbox.activate(new_index)
self.show_image(new_index) self.show_image(new_index)
self.set_upload_button_to_view_or_upload() self.update_button_states()
def show_next(self): def show_next(self):
""" """
@ -772,7 +884,7 @@ class ImageBrowser(tk.Tk):
self.listbox.select_set(new_index) self.listbox.select_set(new_index)
self.listbox.activate(new_index) self.listbox.activate(new_index)
self.show_image(new_index) self.show_image(new_index)
self.set_upload_button_to_view_or_upload() self.update_button_states()
# --- Metody obsługujące widgety z tagami --- # --- Metody obsługujące widgety z tagami ---
@ -781,7 +893,7 @@ class ImageBrowser(tk.Tk):
Aktualizuje widget z tagami z PNG. Aktualizuje widget z tagami z PNG.
Tworzy tagi jako klikalne, zaznaczone lub niezaznaczone. Tworzy tagi jako klikalne, zaznaczone lub niezaznaczone.
""" """
self.png_tags_text.config(state="normal") self.png_tags_text.config(state=tk.NORMAL)
self.png_tags_text.delete("1.0", tk.END) self.png_tags_text.delete("1.0", tk.END)
self.png_tags_states = {} self.png_tags_states = {}
for tag in tags_list: for tag in tags_list:
@ -794,7 +906,7 @@ class ImageBrowser(tk.Tk):
self.png_tags_text.tag_configure(tag_name, foreground="blue") self.png_tags_text.tag_configure(tag_name, foreground="blue")
self.png_tags_text.tag_bind(tag_name, "<Button-1>", self.toggle_png_tag) self.png_tags_text.tag_bind(tag_name, "<Button-1>", self.toggle_png_tag)
self.png_tags_text.insert(tk.INSERT, " ") self.png_tags_text.insert(tk.INSERT, " ")
self.png_tags_text.config(state="disabled") self.png_tags_text.config(state=tk.DISABLED)
self.adjust_text_widget_height(self.png_tags_text) self.adjust_text_widget_height(self.png_tags_text)
self.update_final_tags() self.update_final_tags()
@ -933,9 +1045,9 @@ class ImageBrowser(tk.Tk):
self.png_tags_states[tag] = tag in common self.png_tags_states[tag] = tag in common
tag_name = "png_" + tag tag_name = "png_" + tag
color = "blue" if self.png_tags_states[tag] else "black" color = "blue" if self.png_tags_states[tag] else "black"
self.png_tags_text.config(state="normal") self.png_tags_text.config(state=tk.NORMAL)
self.png_tags_text.tag_configure(tag_name, foreground=color) self.png_tags_text.tag_configure(tag_name, foreground=color)
self.png_tags_text.config(state="disabled") self.png_tags_text.config(state=tk.DISABLED)
for tag in self.tagger_tags_states: for tag in self.tagger_tags_states:
if tag.startswith("character:"): if tag.startswith("character:"):
@ -956,7 +1068,7 @@ class ImageBrowser(tk.Tk):
Poprawia wysokość widgetu. Poprawia wysokość widgetu.
Aktualizuje listę finalnych tagów. Aktualizuje listę finalnych tagów.
""" """
self.tagger_tags_text.config(state="normal") self.tagger_tags_text.config(state=tk.NORMAL)
self.tagger_tags_text.delete("1.0", tk.END) self.tagger_tags_text.delete("1.0", tk.END)
visible_tags = self.get_visible_tags() visible_tags = self.get_visible_tags()
@ -974,7 +1086,7 @@ class ImageBrowser(tk.Tk):
) )
self.tagger_tags_text.insert(tk.INSERT, " ") self.tagger_tags_text.insert(tk.INSERT, " ")
self.tagger_tags_text.config(state="disabled") self.tagger_tags_text.config(state=tk.DISABLED)
self.adjust_text_widget_height(self.tagger_tags_text) self.adjust_text_widget_height(self.tagger_tags_text)
self.update_final_tags() self.update_final_tags()
@ -1035,7 +1147,7 @@ class ImageBrowser(tk.Tk):
final_set.update(manual) final_set.update(manual)
final_list = sorted(final_set) final_list = sorted(final_set)
self.final_tags_text.config(state="normal") self.final_tags_text.config(state=tk.NORMAL)
self.final_tags_text.delete("1.0", tk.END) self.final_tags_text.delete("1.0", tk.END)
for tag in final_list: for tag in final_list:
_, deprecated = process_tag(tag, self.tags_repo) _, deprecated = process_tag(tag, self.tags_repo)
@ -1061,7 +1173,7 @@ class ImageBrowser(tk.Tk):
tag_name, "<Button-1>", self.open_final_tag_wiki_url tag_name, "<Button-1>", self.open_final_tag_wiki_url
) )
self.final_tags_text.insert(tk.INSERT, " ") self.final_tags_text.insert(tk.INSERT, " ")
self.final_tags_text.config(state="disabled") self.final_tags_text.config(state=tk.DISABLED)
def open_final_tag_wiki_url(self, event): def open_final_tag_wiki_url(self, event):
"""Otwiera w przeglądarce URL strony wiki dla klikniętego tagu. """Otwiera w przeglądarce URL strony wiki dla klikniętego tagu.
@ -1101,10 +1213,10 @@ class ImageBrowser(tk.Tk):
if self.current_index is None: if self.current_index is None:
return return
# Ustaw komunikat, że Tagger pracuje # Ustaw komunikat, że Tagger pracuje
self.tagger_tags_text.config(state="normal") self.tagger_tags_text.config(state=tk.NORMAL)
self.tagger_tags_text.delete("1.0", tk.END) self.tagger_tags_text.delete("1.0", tk.END)
self.tagger_tags_text.insert("1.0", "Tagger przetwarza...") self.tagger_tags_text.insert("1.0", "Tagger przetwarza...")
self.tagger_tags_text.config(state="disabled") self.tagger_tags_text.config(state=tk.DISABLED)
file_path = self.image_files[self.current_index] file_path = self.image_files[self.current_index]
result = self.get_tagger_results(file_path) result = self.get_tagger_results(file_path)
new_rating = self.map_tagger_rating(result) new_rating = self.map_tagger_rating(result)
@ -1112,7 +1224,14 @@ class ImageBrowser(tk.Tk):
self.after(0, lambda: self.update_tagger_tags_widget(result)) self.after(0, lambda: self.update_tagger_tags_widget(result))
self.update_listbox_item_color_by_rating(file_path, result.rating) self.update_listbox_item_color_by_rating(file_path, result.rating)
def upload_file(self, file_path, final_tags=None, final_rating=None): def upload_file(
self, file_path, final_tags=None, final_rating=None, progress_queue=None
):
base_file_name = os.path.basename(file_path)
if progress_queue:
progress_queue.put(("mode", "determinate"))
progress_queue.put(("max", 100))
progress_queue.put(("label", f"Wysyłam plik {base_file_name}..."))
url = self.settings.base_url.rstrip("/") + "/api/danbooru/add_post" url = self.settings.base_url.rstrip("/") + "/api/danbooru/add_post"
tags = ( tags = (
self.final_tags_text.get("1.0", tk.END).strip() self.final_tags_text.get("1.0", tk.END).strip()
@ -1134,16 +1253,16 @@ class ImageBrowser(tk.Tk):
total_size = os.path.getsize(file_path) total_size = os.path.getsize(file_path)
def progress_callback(bytes_read, total_size): def progress_callback(bytes_read, total_size):
if progress_queue:
percentage = int(bytes_read / total_size * 100) percentage = int(bytes_read / total_size * 100)
self.progress_bar.after(0, lambda: self.progress_var.set(percentage)) progress_queue.put(("progress", percentage))
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
wrapped_file = ProgressFile(f, progress_callback, total_size) wrapped_file = ProgressFile(f, progress_callback, total_size)
files = { files = {"file": (base_file_name, wrapped_file, "image/png")}
"file": (os.path.basename(file_path), wrapped_file, "image/png")
}
response = requests.post(url, data=fields, files=files) response = requests.post(url, data=fields, files=files)
self.progress_bar.after(0, lambda: self.progress_var.set(100)) if progress_queue:
progress_queue.put(("progress", 100))
show_warn = False show_warn = False
post_url = None post_url = None
if response.status_code in (200, 201): if response.status_code in (200, 201):
@ -1156,9 +1275,6 @@ class ImageBrowser(tk.Tk):
else: else:
message = f"Upload zakończony błędem.\nStatus: {response.status_code}\nTreść: {response.text}" message = f"Upload zakończony błędem.\nStatus: {response.status_code}\nTreść: {response.text}"
show_warn = True show_warn = True
self.upload_button.after(
0, lambda: self.upload_button.config(state="normal")
)
# Aktualizacja wyglądu listy musimy użyć domyślnych argumentów w lambdzie, aby zachować bieżący indeks # Aktualizacja wyglądu listy musimy użyć domyślnych argumentów w lambdzie, aby zachować bieżący indeks
if show_warn: if show_warn:
if not final_tags: if not final_tags:
@ -1177,13 +1293,101 @@ class ImageBrowser(tk.Tk):
self.uploaded[file_path] = post_id self.uploaded[file_path] = post_id
self.uploaded_count += 1 self.uploaded_count += 1
self.after(0, self.update_status_bar) self.after(0, self.update_status_bar)
self.set_upload_button_to_view_or_upload() self.after(0, self.update_button_states)
except Exception as e: except Exception as e:
self.upload_button.after( self.upload_button.after(0, self.update_button_states)
0, lambda: self.upload_button.config(state="normal")
)
messagebox.showerror("Błąd uploadu", str(e)) messagebox.showerror("Błąd uploadu", str(e))
def edit_file(
self, file_path, final_tags=None, final_rating=None, progress_queue=None
):
"""
Update tags and rating for an existing post without uploading the file.
Args:
file_path: Path to the image file associated with the post
final_tags: Optional override tags (space-separated string)
final_rating: Optional override rating
progress_queue: Progress communication queue
Requires:
- The file must have an existing post ID in self.uploaded
"""
base_file_name = os.path.basename(file_path)
post_id = self.uploaded.get(file_path)
if not post_id:
messagebox.showerror(
"Błąd edycji", "Post nie został znaleziony dla tego pliku"
)
return
if progress_queue:
progress_queue.put(("mode", "determinate"))
progress_queue.put(("max", 100))
progress_queue.put(("label", f"Aktualizuję tagi dla {base_file_name}..."))
try:
# Get authentication session and token
session = login(self.settings)
auth_token = get_auth_token(session, self.settings)
# Prepare tags and rating
tags = (
self.final_tags_text.get("1.0", tk.END).strip()
if final_tags is None
else final_tags
)
rating_value = self.rating_map.get(
self.rating_var.get() if final_rating is None else final_rating, "?"
)
# Prepare API request
url = self.settings.base_url.rstrip("/") + "/post/set"
payload = {
"auth_token": auth_token,
"image_id": post_id,
"title": base_file_name,
"owner": self.settings.username,
"tags": tags,
"source": "",
"rating": rating_value,
}
if progress_queue:
progress_queue.put(("progress", 50))
# Send update request
response = session.post(url, data=payload, allow_redirects=False)
# Handle 302 redirect as success case
if response.status_code == 302:
if progress_queue:
progress_queue.put(("progress", 100))
message = "Tagi zostały zaktualizowane!"
if not final_tags: # Only show success if not bulk operation
messagebox.showinfo("Sukces edycji", message)
# Update UI state
self.after(0, self.update_button_states)
return
# Handle other status codes
error_msg = (
f"Błąd podczas aktualizacji tagów\nStatus: {response.status_code}"
)
if response.text:
error_msg += f"\nTreść: {response.text}"
messagebox.showerror("Błąd edycji", error_msg)
except Exception as e:
messagebox.showerror("Krytyczny błąd edycji", str(e))
finally:
if progress_queue:
progress_queue.put(("progress", 100))
def upload_all_files(self): def upload_all_files(self):
""" """
Metoda, która po potwierdzeniu przez użytkownika uploaduje wszystkie niewrzucone pliki. Metoda, która po potwierdzeniu przez użytkownika uploaduje wszystkie niewrzucone pliki.

View File

@ -179,20 +179,24 @@ class TagManager(tk.Frame):
based on custom logic. based on custom logic.
""" """
def __init__(self, master, settings: Settings, tags_repo: TagsRepo, *args, **kwargs): def __init__(
self, master, settings: Settings, tags_repo: TagsRepo, *args, **kwargs
):
super().__init__(master, *args, **kwargs) super().__init__(master, *args, **kwargs)
self.tags_repo = tags_repo self.tags_repo = tags_repo
self.settings = settings self.settings = settings
self.manual_tags = [] # List to hold manually entered tags self.manual_tags = [] # List to hold manually entered tags
# Entry for new tags (with autocompletion) # Entry for new tags (with autocompletion)
self.entry = AutocompleteEntry(self, callback=self.add_tag, tags_repo=self.tags_repo) self.entry = AutocompleteEntry(
self, callback=self.add_tag, tags_repo=self.tags_repo
)
self.entry.pack(fill=tk.X, padx=5, pady=5) self.entry.pack(fill=tk.X, padx=5, pady=5)
# Text widget for displaying already entered tags # Text widget for displaying already entered tags
self.tags_display = tk.Text(self, wrap="word", height=4) self.tags_display = tk.Text(self, wrap=tk.WORD, height=4)
self.tags_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.tags_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tags_display.config(state="disabled") self.tags_display.config(state=tk.DISABLED)
# (Optional: add a scrollbar if needed) # (Optional: add a scrollbar if needed)
def add_tag(self, tag): def add_tag(self, tag):
@ -229,7 +233,7 @@ class TagManager(tk.Frame):
self.tags_display.tag_bind(tag_name, "<Button-1>", self.remove_tag) 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.tag_bind(tag_name, "<Button-3>", self.open_tag_wiki_url)
self.tags_display.insert(tk.INSERT, " ") self.tags_display.insert(tk.INSERT, " ")
self.tags_display.config(state="disabled") self.tags_display.config(state=tk.DISABLED)
def remove_tag(self, event): def remove_tag(self, event):
"""Remove the clicked tag from the list and update the display.""" """Remove the clicked tag from the list and update the display."""

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kapitanbooru-uploader" name = "kapitanbooru-uploader"
version = "0.1.0" version = "0.2.0"
description = "A GUI application for uploading images to KapitanBooru" description = "A GUI application for uploading images to KapitanBooru"
authors = [ authors = [
{name = "Michał Leśniak", email = "kapitan@mlesniak.pl"} {name = "Michał Leśniak", email = "kapitan@mlesniak.pl"}