Translations, suggestion box, UX
All checks were successful
Gitea/kapitanbooru-uploader/pipeline/head This commit looks good

This commit is contained in:
Michał Leśniak 2025-03-03 00:47:38 +01:00
parent 9f187afc22
commit 9361bc0363
17 changed files with 1515 additions and 135 deletions

1
.gitignore vendored
View File

@ -54,6 +54,7 @@ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
*.po~
# Django stuff: # Django stuff:
*.log *.log

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"i18n-ally.localesPaths": [
"locales"
]
}

14
Jenkinsfile vendored
View File

@ -1,5 +1,5 @@
pipeline { pipeline {
agent { label 'Pi4' } // Use Raspberry Pi 4 agent agent { label 'Pi4' } // Use Raspberry Pi 4 agent running on Ubuntu Server LTS 24.04
environment { environment {
PIP_EXTRA_INDEX_URL = 'http://localhost:8090/simple/' // Local PyPI repo PIP_EXTRA_INDEX_URL = 'http://localhost:8090/simple/' // Local PyPI repo
PACKAGE_NAME = 'kapitanbooru_uploader' // Your package name PACKAGE_NAME = 'kapitanbooru_uploader' // Your package name
@ -17,6 +17,18 @@ pipeline {
sh '. venv/bin/activate && pip install --upgrade pip build twine' sh '. venv/bin/activate && pip install --upgrade pip build twine'
} }
} }
stage('Compile Translations') {
steps {
// Find all .po files under kapitanbooru_uploader/locales and compile them to .mo
sh '''
find kapitanbooru_uploader/locales -name "*.po" -print0 | while IFS= read -r -d '' po; do
mo=$(echo "$po" | sed 's/\\.po$/.mo/');
msgfmt "$po" -o "$mo";
done
'''
}
}
stage('Build Package') { stage('Build Package') {
steps { steps {

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
recursive-include kapitanbooru_uploader/locales *.mo

View File

@ -0,0 +1,50 @@
import gettext
import locale
import os
from typing import Dict
class I18N:
def __init__(self, locale_dir=None):
# If no locale_dir is provided, use the locales folder relative to this file.
if locale_dir is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
locale_dir = os.path.join(current_dir, "locales")
self.locale_dir = locale_dir
self.languages = {"en": "English", "pl": "Polski"}
self.current_lang = "en"
self.translations: Dict[str, str] = {}
self.load_translations()
def load_translations(self):
"""Load all available translations"""
for lang in self.languages:
try:
trans = gettext.translation(
"messages", localedir=self.locale_dir, languages=[lang]
)
self.translations[lang] = trans.gettext
except FileNotFoundError:
self.translations[lang] = lambda x: x
def set_language(self, lang: str):
"""Set application language"""
if lang in self.languages:
self.current_lang = lang
# For Windows language detection
try:
locale.setlocale(locale.LC_ALL, self.get_locale_code(lang))
except locale.Error:
pass
def get_locale_code(self, lang: str) -> str:
"""Map languages to locale codes"""
return {"en": "en_US.UTF-8", "pl": "pl_PL.UTF-8"}.get(lang, "en_US.UTF-8")
def __call__(self, text: str) -> str:
"""Translate text using current language"""
return self.translations[self.current_lang](text)
# Shortcut for translation function
_ = I18N()

View File

@ -11,8 +11,9 @@ from typing import Dict, Tuple, Optional
import networkx as nx import networkx as nx
import requests import requests
from PIL import Image, ImageTk, PngImagePlugin from PIL import Image, ImageTk, PngImagePlugin
import wdtagger as wdt import wdtagger as wdt
from .I18N import _
from .ProgressFile import ProgressFile from .ProgressFile import ProgressFile
from .TagsRepo import TagsRepo from .TagsRepo import TagsRepo
from .autocomplete import TagManager from .autocomplete import TagManager
@ -26,11 +27,11 @@ 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) 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) self.top.protocol("WM_DELETE_WINDOW", self.on_close)
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 # Start with indeterminate progress bar
@ -38,8 +39,15 @@ class ProcessingDialog:
self.progress.pack(pady=10, fill="x") self.progress.pack(pady=10, fill="x")
self.progress.start(10) self.progress.start(10)
sig = inspect.signature(target_function)
if "secondary_progress_queue" in sig.parameters:
self.sub_progress = ttk.Progressbar(self.top, mode="indeterminate")
self.sub_progress.pack(pady=10, fill="x")
self.sub_progress.start(10)
# Setup communication queue and periodic checker # Setup communication queue and periodic checker
self.queue = queue.Queue() self.queue = queue.Queue()
self.sub_queue = queue.Queue()
self.running = True self.running = True
self.cancel_event = threading.Event() # Cancellation flag self.cancel_event = threading.Event() # Cancellation flag
self.thread = threading.Thread( self.thread = threading.Thread(
@ -58,6 +66,10 @@ class ProcessingDialog:
self.progress.config(mode=msg[1]) self.progress.config(mode=msg[1])
if msg[1] == "determinate": if msg[1] == "determinate":
self.progress["value"] = 0 self.progress["value"] = 0
self.progress.stop()
elif msg[1] == "indeterminate":
self.progress["value"] = 0
self.progress.start()
elif msg[0] == "max": elif msg[0] == "max":
self.progress["maximum"] = msg[1] self.progress["maximum"] = msg[1]
elif msg[0] == "progress": elif msg[0] == "progress":
@ -69,6 +81,26 @@ class ProcessingDialog:
except queue.Empty: except queue.Empty:
break break
try:
msg = self.sub_queue.get_nowait()
if msg[0] == "mode":
self.sub_progress.config(mode=msg[1])
if msg[1] == "determinate":
self.sub_progress["value"] = 0
self.sub_progress.stop()
elif msg[1] == "indeterminate":
self.sub_progress["value"] = 0
self.sub_progress.start()
elif msg[0] == "max":
self.sub_progress["maximum"] = msg[1]
elif msg[0] == "progress":
self.sub_progress["value"] = msg[1]
self.top.update_idletasks()
except queue.Empty:
break
if self.running: if self.running:
self.top.after(100, self.process_queue) self.top.after(100, self.process_queue)
@ -79,6 +111,8 @@ class ProcessingDialog:
kwargs = {} kwargs = {}
if "progress_queue" in sig.parameters: if "progress_queue" in sig.parameters:
kwargs["progress_queue"] = self.queue kwargs["progress_queue"] = self.queue
if "secondary_progress_queue" in sig.parameters:
kwargs["secondary_progress_queue"] = self.sub_queue
if "cancel_event" in sig.parameters: if "cancel_event" in sig.parameters:
kwargs["cancel_event"] = self.cancel_event kwargs["cancel_event"] = self.cancel_event
target_function(*args, **kwargs) target_function(*args, **kwargs)
@ -91,7 +125,7 @@ class ProcessingDialog:
self.running = False self.running = False
self.progress.stop() self.progress.stop()
self.top.after(0, self.top.destroy) self.top.after(0, self.top.destroy)
self.thread.join() self.root.after(100, self.thread.join)
def on_close(self): def on_close(self):
"""Handle manual window closure""" """Handle manual window closure"""
@ -105,6 +139,7 @@ class ImageBrowser(tk.Tk):
super().__init__() super().__init__()
self.title("Kapitanbooru Uploader") self.title("Kapitanbooru Uploader")
self.geometry("900x600") self.geometry("900x600")
self.version = "0.4.0"
self.settings = Settings() self.settings = Settings()
self.tags_repo = TagsRepo(self.settings) self.tags_repo = TagsRepo(self.settings)
@ -169,6 +204,15 @@ class ImageBrowser(tk.Tk):
self.create_widgets() self.create_widgets()
self.bind_events() self.bind_events()
def reload_ui(self):
"""Reload UI components with new language"""
# Destroy current widgets
for widget in self.winfo_children():
widget.destroy()
# Rebuild UI
self.create_menu()
self.create_widgets()
def load_implication_graph(self) -> nx.DiGraph: def load_implication_graph(self) -> nx.DiGraph:
G = nx.DiGraph() G = nx.DiGraph()
conn = self.tags_repo.get_conn() conn = self.tags_repo.get_conn()
@ -249,7 +293,7 @@ class ImageBrowser(tk.Tk):
png_tags = set(parse_parameters(parameters, self.tags_repo).split()) png_tags = set(parse_parameters(parameters, self.tags_repo).split())
img.close() img.close()
except Exception as e: except Exception as e:
print("Błąd przy otwieraniu pliku", file_path, ":", e) print(_("Błąd przy otwieraniu pliku"), file_path, ":", e)
png_tags = set() png_tags = set()
# Pobierz tagi z Taggera sprawdzając cache # Pobierz tagi z Taggera sprawdzając cache
@ -291,10 +335,10 @@ class ImageBrowser(tk.Tk):
self.tagger_cache[md5] = result self.tagger_cache[md5] = result
self.tagger_processed.add(md5) self.tagger_processed.add(md5)
self.after(0, self.update_status_bar) self.after(0, self.update_status_bar)
print(f"Tagger przetworzył: {file_path}") print(_("Tagger przetworzył:"), f"{file_path}")
return result return result
except Exception as e: except Exception as e:
print("Błąd Taggera dla", file_path, ":", e) print(_("Błąd Taggera dla"), file_path, ":", e)
def map_tagger_rating(self, result: wdt.Result) -> str: def map_tagger_rating(self, result: wdt.Result) -> str:
""" """
@ -320,37 +364,98 @@ class ImageBrowser(tk.Tk):
self.file_menu = tk.Menu(menubar, tearoff=0) self.file_menu = tk.Menu(menubar, tearoff=0)
# File menu items - create references for items we need to control # 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_command(label=_("Otwórz folder"), command=self.select_folder)
self.file_menu.add_separator() self.file_menu.add_separator()
self.file_menu.add_command( self.file_menu.add_command(
label="Wyślij", command=self.upload_current_image, state=tk.DISABLED label=_("Wyślij"), command=self.upload_current_image, state=tk.DISABLED
) )
self.file_menu.add_command( self.file_menu.add_command(
label="Wyślij wszystko", command=self.upload_all_files, state=tk.DISABLED label=_("Wyślij wszystko"), command=self.upload_all_files, state=tk.DISABLED
) )
self.file_menu.add_separator() self.file_menu.add_separator()
self.file_menu.add_command( self.file_menu.add_command(
label="Podmień tagi", command=self.edit_current_image, state=tk.DISABLED label=_("Podmień tagi"), command=self.edit_current_image, state=tk.DISABLED
) )
self.file_menu.add_command( self.file_menu.add_command(
label="Otwórz post", command=self.view_current_post, state=tk.DISABLED label=_("Otwórz post"), command=self.view_current_post, state=tk.DISABLED
) )
self.file_menu.add_separator() self.file_menu.add_separator()
self.file_menu.add_command(label="Zakończ", command=self.quit) self.file_menu.add_command(label=_("Zakończ"), command=self.quit)
menubar.add_cascade(label="Plik", menu=self.file_menu) menubar.add_cascade(label=_("Plik"), menu=self.file_menu)
# Options menu # Options menu
options_menu = tk.Menu(menubar, tearoff=0) options_menu = tk.Menu(menubar, tearoff=0)
options_menu.add_command(label="Ustawienia", command=self.open_settings) options_menu.add_command(label=_("Ustawienia"), command=self.open_settings)
options_menu.add_separator() options_menu.add_separator()
options_menu.add_command( options_menu.add_command(
label="Wyczyść cache Taggera", command=self.clear_cache label=_("Wyczyść cache Taggera"), command=self.clear_cache
) )
options_menu.add_command( 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) menubar.add_cascade(label=_("Opcje"), menu=options_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label=_("About"), command=self.show_about)
menubar.add_cascade(label=_("Help"), menu=help_menu)
def show_about(self):
"""Multilingual About window (updated version)"""
about = tk.Toplevel(self)
about.title(_("About Kapitanbooru Uploader"))
about.resizable(False, False)
# Window content
frame = ttk.Frame(about, padding=10)
frame.pack(fill="both", expand=True)
ttk.Label(
frame, text="Kapitanbooru Uploader", font=("TkDefaultFont", 16, "bold")
).grid(row=0, column=0, sticky=tk.W)
content = [
(f"Version {self.version}", 1),
("", 2),
(_("A GUI application for uploading images to KapitanBooru."), 3),
(_("Features include image upload, tag management, automatic"), 4),
(_("tagging with wdtagger, and cache management."), 5),
("", 6),
(_("Authors:"), 7),
("Michał Leśniak", 8),
("", 9),
(_("License: MIT License"), 10),
(f"Copyright © 2025 Michał Leśniak", 11),
("", 12),
]
for text, row in content:
ttk.Label(frame, text=text).grid(row=row, column=0, sticky=tk.W)
# Repository link
repo_frame = ttk.Frame(frame)
repo_frame.grid(row=13, sticky=tk.W)
ttk.Label(repo_frame, text=_("Repository:")).pack(side=tk.LEFT)
repo_url = "https://git.mlesniak.pl/kapitan/kapitanbooru-uploader"
repo = ttk.Label(repo_frame, text=repo_url, cursor="hand2", foreground="blue")
repo.pack(side=tk.LEFT, padx=(5, 0))
repo.bind("<Button-1>", lambda e: open_webbrowser(repo_url, self.settings))
# Website link
website_frame = ttk.Frame(frame)
website_frame.grid(row=14, sticky=tk.W)
ttk.Label(website_frame, text=_("Website:")).pack(side=tk.LEFT)
website_url = "https://booru.mlesniak.pl"
website = ttk.Label(
website_frame, text=website_url, cursor="hand2", foreground="blue"
)
website.pack(side=tk.LEFT, padx=(5, 0))
website.bind(
"<Button-1>", lambda e: open_webbrowser(website_url, self.settings)
)
# Close button
ttk.Button(about, text=_("Close"), command=about.destroy).pack(pady=10)
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)
@ -358,41 +463,44 @@ class ImageBrowser(tk.Tk):
def clear_cache(self): def clear_cache(self):
res, err = self.tagger_cache.clear_cache() res, err = self.tagger_cache.clear_cache()
if res: if res:
messagebox.showinfo("Cache", "Cache Taggera zostało wyczyszczone.") messagebox.showinfo(_("Cache"), _("Cache Taggera zostało wyczyszczone."))
else: else:
messagebox.showerror("Cache", f"Błąd przy czyszczeniu cache: {err}") messagebox.showerror(
_("Cache"), f"{_('Błąd przy czyszczeniu cache:')} {err}"
)
def open_settings(self): def open_settings(self):
settings_window = tk.Toplevel(self) settings_window = tk.Toplevel(self)
settings_window.title("Ustawienia") settings_window.title(_("Ustawienia"))
settings_window.geometry("300x350") settings_window.geometry("300x430") # Enlarged vertically
settings_window.resizable(False, False) # Disable resizing
settings_window.grab_set() settings_window.grab_set()
lbl_login = tk.Label(settings_window, text="Login:") lbl_login = tk.Label(settings_window, text=_("Login:"))
lbl_login.pack(pady=(10, 0)) lbl_login.pack(pady=(10, 0))
entry_login = tk.Entry(settings_window) entry_login = tk.Entry(settings_window)
entry_login.pack(pady=(0, 10), padx=10, fill="x") entry_login.pack(pady=(0, 10), padx=10, fill="x")
entry_login.insert(0, self.settings.username) entry_login.insert(0, self.settings.username)
lbl_password = tk.Label(settings_window, text="Hasło:") lbl_password = tk.Label(settings_window, text=_("Hasło:"))
lbl_password.pack(pady=(10, 0)) lbl_password.pack(pady=(10, 0))
entry_password = tk.Entry(settings_window, show="*") entry_password = tk.Entry(settings_window, show="*")
entry_password.pack(pady=(0, 10), padx=10, fill="x") entry_password.pack(pady=(0, 10), padx=10, fill="x")
entry_password.insert(0, self.settings.password) entry_password.insert(0, self.settings.password)
lbl_base_url = tk.Label(settings_window, text="Base URL:") lbl_base_url = tk.Label(settings_window, text=_("Base URL:"))
lbl_base_url.pack(pady=(10, 0)) lbl_base_url.pack(pady=(10, 0))
entry_base_url = tk.Entry(settings_window) entry_base_url = tk.Entry(settings_window)
entry_base_url.pack(pady=(0, 10), padx=10, fill="x") entry_base_url.pack(pady=(0, 10), padx=10, fill="x")
entry_base_url.insert(0, self.settings.base_url) entry_base_url.insert(0, self.settings.base_url)
lbl_default_tags = tk.Label(settings_window, text="Default Tags:") lbl_default_tags = tk.Label(settings_window, text=_("Default Tags:"))
lbl_default_tags.pack(pady=(10, 0)) lbl_default_tags.pack(pady=(10, 0))
entry_default_tags = tk.Entry(settings_window) entry_default_tags = tk.Entry(settings_window)
entry_default_tags.pack(pady=(0, 10), padx=10, fill="x") entry_default_tags.pack(pady=(0, 10), padx=10, fill="x")
entry_default_tags.insert(0, self.settings.default_tags) entry_default_tags.insert(0, self.settings.default_tags)
lbl_browser = tk.Label(settings_window, text="Browser:") lbl_browser = tk.Label(settings_window, text=_("Browser:"))
lbl_browser.pack(pady=(10, 0)) lbl_browser.pack(pady=(10, 0))
cb_browser = ttk.Combobox( cb_browser = ttk.Combobox(
settings_window, settings_window,
@ -406,16 +514,39 @@ class ImageBrowser(tk.Tk):
) )
) )
lbl_lang = tk.Label(settings_window, text=_("Language:"))
lbl_lang.pack(pady=(10, 0))
cb_lang = ttk.Combobox(
settings_window,
values=list(self.settings.i18n.languages.values()),
state="readonly",
)
cb_lang.pack(pady=(0, 10), padx=10, fill="x")
cb_lang.set(
self.settings.i18n.languages.get(self.settings.i18n.current_lang, "en")
)
def save_and_close(): def save_and_close():
self.settings.username = entry_login.get() self.settings.username = entry_login.get()
self.settings.password = entry_password.get() self.settings.password = entry_password.get()
self.settings.base_url = entry_base_url.get() self.settings.base_url = entry_base_url.get()
self.settings.default_tags = entry_default_tags.get() self.settings.default_tags = entry_default_tags.get()
self.settings.browser = self.settings.installed_browsers[cb_browser.get()] self.settings.browser = self.settings.installed_browsers[cb_browser.get()]
self.settings.i18n.set_language(
next(
(
lang
for lang, name in self.settings.i18n.languages.items()
if name == cb_lang.get()
),
"en",
)
)
self.settings.save_settings() self.settings.save_settings()
settings_window.destroy() settings_window.destroy()
self.reload_ui()
btn_save = tk.Button(settings_window, text="Zapisz", command=save_and_close) btn_save = tk.Button(settings_window, text=_("Zapisz"), command=save_and_close)
btn_save.pack(pady=10) btn_save.pack(pady=10)
def create_widgets(self): def create_widgets(self):
@ -457,7 +588,7 @@ class ImageBrowser(tk.Tk):
right_frame.grid_rowconfigure(4, weight=0) # Upload Panel right_frame.grid_rowconfigure(4, weight=0) # Upload Panel
# 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=tk.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=tk.WORD) self.png_tags_text = tk.Text(png_frame, wrap=tk.WORD)
@ -469,7 +600,7 @@ class ImageBrowser(tk.Tk):
) )
# 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=tk.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=tk.WORD) self.tagger_tags_text = tk.Text(tagger_frame, wrap=tk.WORD)
@ -483,7 +614,7 @@ class ImageBrowser(tk.Tk):
) )
# 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=tk.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, self.update_final_tags manual_frame, self.settings, self.tags_repo, self.update_final_tags
@ -491,7 +622,7 @@ class ImageBrowser(tk.Tk):
self.manual_tags_manager.pack(fill=tk.BOTH, expand=True) self.manual_tags_manager.pack(fill=tk.BOTH, expand=True)
# 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=tk.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)
@ -511,12 +642,12 @@ class ImageBrowser(tk.Tk):
) )
self.rating_dropdown.pack(side=tk.LEFT, padx=5) self.rating_dropdown.pack(side=tk.LEFT, padx=5)
self.upload_button = tk.Button( self.upload_button = tk.Button(
upload_frame, text="Upload", command=self.upload_current_image upload_frame, text=_("Wyślij"), command=self.upload_current_image
) )
self.upload_button.pack(side=tk.LEFT, padx=5) self.upload_button.pack(side=tk.LEFT, padx=5)
self.upload_button.config(state=tk.DISABLED) self.upload_button.config(state=tk.DISABLED)
self.view_post_button = tk.Button( self.view_post_button = tk.Button(
upload_frame, text="Wyświetl", command=self.view_current_post upload_frame, text=_("Wyświetl"), command=self.view_current_post
) )
self.view_post_button.pack(side=tk.LEFT, padx=5) self.view_post_button.pack(side=tk.LEFT, padx=5)
self.view_post_button.config(state=tk.DISABLED) self.view_post_button.config(state=tk.DISABLED)
@ -533,9 +664,9 @@ class ImageBrowser(tk.Tk):
def update_status_bar(self): def update_status_bar(self):
status_text = ( status_text = (
f"Przetworzono tagi: {len(self.tagger_processed)}/{self.total_files} plików | " f"{_('Przetworzono tagi:')} {len(self.tagger_processed)}/{self.total_files} {_('plików')} | "
f"Zweryfikowano status uploadu: {self.upload_verified}/{self.total_files} plików | " f"{_('Zweryfikowano status uploadu:')} {self.upload_verified}/{self.total_files} {_('plików')} | "
f"Zuploadowano: {self.uploaded_count}/{self.upload_verified} plików" f"{_('Zuploadowano:')} {self.uploaded_count}/{self.upload_verified} {_('plików')}"
) )
self.status_label.config(text=status_text) self.status_label.config(text=status_text)
@ -563,7 +694,7 @@ class ImageBrowser(tk.Tk):
Otwiera okno dialogowe wyboru folderu z obrazkami Otwiera okno dialogowe wyboru folderu z obrazkami
i wczytuje pliki PNG z wybranego folderu. i wczytuje pliki PNG z wybranego folderu.
""" """
folder = filedialog.askdirectory(title="Wybierz folder z obrazkami PNG") folder = filedialog.askdirectory(title=_("Wybierz folder z obrazkami PNG"))
if folder: if folder:
self.folder_path = folder self.folder_path = folder
self.load_images() self.load_images()
@ -593,7 +724,9 @@ class ImageBrowser(tk.Tk):
self.show_image(0) self.show_image(0)
self.post_load_processing() self.post_load_processing()
else: else:
messagebox.showinfo("Informacja", "Brak plików PNG w wybranym folderze.") messagebox.showinfo(
_("Informacja"), _("Brak plików PNG w wybranym folderze.")
)
def post_load_processing(self): def post_load_processing(self):
""" """
@ -675,7 +808,7 @@ class ImageBrowser(tk.Tk):
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)
self.after(100, self.join_check_uploaded_files_thread) self.after(100, self.join_check_uploaded_files_thread)
def update_button_states(self): def update_button_states(self):
@ -716,14 +849,14 @@ class ImageBrowser(tk.Tk):
post_ops_state = tk.NORMAL if post_id else tk.DISABLED post_ops_state = tk.NORMAL if post_id else tk.DISABLED
# Update menu items # Update menu items
self.file_menu.entryconfig("Wyślij wszystko", state=send_all_state) 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(_("Podmień tagi"), state=post_ops_state)
self.file_menu.entryconfig("Otwórz post", state=post_ops_state) self.file_menu.entryconfig(_("Otwórz post"), state=post_ops_state)
# Update buttons # Update buttons
self.upload_button.config( self.upload_button.config(
state=tk.NORMAL if has_current else tk.DISABLED, state=tk.NORMAL if has_current else tk.DISABLED,
text="Podmień tagi" if post_id else "Wyślij", text=_("Podmień tagi") if post_id else _("Wyślij"),
command=self.edit_current_image if post_id else self.upload_current_image, command=self.edit_current_image if post_id else self.upload_current_image,
) )
self.view_post_button.config(state=post_ops_state) self.view_post_button.config(state=post_ops_state)
@ -731,7 +864,7 @@ class ImageBrowser(tk.Tk):
# Special case for "Wyślij" menu item # Special case for "Wyślij" menu item
wyślij_state = tk.DISABLED if post_id else tk.NORMAL wyślij_state = tk.DISABLED if post_id else tk.NORMAL
self.file_menu.entryconfig( self.file_menu.entryconfig(
"Wyślij", state=wyślij_state if has_current else tk.DISABLED _("Wyślij"), state=wyślij_state if has_current else tk.DISABLED
) )
def view_current_post(self): def view_current_post(self):
@ -751,7 +884,7 @@ class ImageBrowser(tk.Tk):
for chunk in iter(lambda: f.read(chunk_size), b""): for chunk in iter(lambda: f.read(chunk_size), b""):
hash_md5.update(chunk) hash_md5.update(chunk)
except Exception as e: except Exception as e:
print("Błąd przy obliczaniu MD5:", e) print(_("Błąd przy obliczaniu MD5:"), e)
return "" return ""
return hash_md5.hexdigest() return hash_md5.hexdigest()
@ -807,7 +940,7 @@ class ImageBrowser(tk.Tk):
self.tagger_thread_idx += 1 self.tagger_thread_idx += 1
thread.start() thread.start()
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): def edit_current_image(self):
""" """
@ -1016,13 +1149,21 @@ class ImageBrowser(tk.Tk):
if tag in self.implication_graph: if tag in self.implication_graph:
implied_by_selected.update(nx.descendants(self.implication_graph, tag)) implied_by_selected.update(nx.descendants(self.implication_graph, tag))
else: else:
print(f"Warning: Tag '{tag}' not found in implication graph") print(
_("Warning: Tag '{tag}' not found in implication graph").format(
tag=tag
)
)
self.missing_tags.add(tag) # Log missing tags self.missing_tags.add(tag) # Log missing tags
for tag in selected_png_tags: for tag in selected_png_tags:
if tag in self.implication_graph: if tag in self.implication_graph:
implied_by_selected.update(nx.descendants(self.implication_graph, tag)) implied_by_selected.update(nx.descendants(self.implication_graph, tag))
else: else:
print(f"Warning: Tag '{tag}' not found in implication graph") print(
_("Warning: Tag '{tag}' not found in implication graph").format(
tag=tag
)
)
self.missing_tags.add(tag) # Log missing tags self.missing_tags.add(tag) # Log missing tags
# Build visible list # Build visible list
@ -1249,7 +1390,7 @@ class ImageBrowser(tk.Tk):
# Ustaw komunikat, że Tagger pracuje # Ustaw komunikat, że Tagger pracuje
self.tagger_tags_text.config(state=tk.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=tk.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)
@ -1264,14 +1405,21 @@ class ImageBrowser(tk.Tk):
file_path, file_path,
final_tags=None, final_tags=None,
final_rating=None, final_rating=None,
progress_queue=None, progress_queue: Optional[queue.Queue] = None,
cancel_event=None, cancel_event: Optional[threading.Event] = None,
): ):
base_file_name = os.path.basename(file_path) base_file_name = os.path.basename(file_path)
if progress_queue: if progress_queue:
progress_queue.put(("mode", "determinate")) progress_queue.put(("mode", "determinate"))
progress_queue.put(("max", 100)) progress_queue.put(("max", 100))
progress_queue.put(("label", f"Wysyłam plik {base_file_name}...")) progress_queue.put(
(
"label",
_("Wysyłam plik {base_file_name}...").format(
base_file_name=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()
@ -1308,22 +1456,29 @@ class ImageBrowser(tk.Tk):
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):
message = "Upload zakończony powodzeniem!" message = _("Wysyłanie zakończone powodzeniem!")
post_url = response.headers.get("X-Danbooru-Location", None) post_url = response.headers.get("X-Danbooru-Location", None)
elif response.status_code == 409: elif response.status_code == 409:
message = f"Upload zakończony błędem.\nStatus: 409\nTreść: {response.headers.get('X-Danbooru-Errors', '')}" message = _(
"Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}"
).format(
status_code=response.status_code,
text=response.headers.get("X-Danbooru-Errors", ""),
)
post_url = response.headers.get("X-Danbooru-Location", None) post_url = response.headers.get("X-Danbooru-Location", None)
show_warn = True show_warn = True
else: else:
message = f"Upload zakończony błędem.\nStatus: {response.status_code}\nTreść: {response.text}" message = _(
"Wysyłanie zakończone błędem.\nStatus: {status_code}\nTreść: {text}"
).format(status_code=response.status_code, text=response.text)
show_warn = True show_warn = True
# 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:
messagebox.showwarning("Upload", message) messagebox.showwarning(_("Wysyłanie"), message)
else: else:
if not final_tags: if not final_tags:
messagebox.showinfo("Upload", message) messagebox.showinfo(_("Wysyłanie"), message)
self.after( self.after(
0, 0,
lambda idx=self.image_files.index( lambda idx=self.image_files.index(
@ -1336,7 +1491,7 @@ class ImageBrowser(tk.Tk):
self.uploaded_count += 1 self.uploaded_count += 1
self.after(0, self.update_status_bar) self.after(0, self.update_status_bar)
except Exception as e: except Exception as e:
messagebox.showerror("Błąd uploadu", str(e)) messagebox.showerror(_("Błąd wysyłania"), str(e))
finally: finally:
self.upload_button.after(0, self.update_button_states) self.upload_button.after(0, self.update_button_states)
@ -1356,20 +1511,27 @@ class ImageBrowser(tk.Tk):
if not post_id: if not post_id:
messagebox.showerror( messagebox.showerror(
"Błąd edycji", "Post nie został znaleziony dla tego pliku" _("Błąd edycji"), _("Post nie został znaleziony dla tego pliku")
) )
return return
if progress_queue: if progress_queue:
progress_queue.put(("mode", "determinate")) progress_queue.put(("mode", "determinate"))
progress_queue.put(("max", 100)) progress_queue.put(("max", 100))
progress_queue.put(("label", f"Aktualizuję tagi dla {base_file_name}...")) progress_queue.put(
(
"label",
_("Aktualizuję tagi dla {base_file_name}...").format(
base_file_name=base_file_name
),
)
)
try: try:
# Check for cancellation before starting the operation. # Check for cancellation before starting the operation.
if cancel_event is not None and cancel_event.is_set(): if cancel_event is not None and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Operacja anulowana")) progress_queue.put(("label", _("Operacja anulowana")))
return return
# Get authentication session and token # Get authentication session and token
@ -1379,7 +1541,7 @@ class ImageBrowser(tk.Tk):
# Check cancellation after login if needed. # Check cancellation after login if needed.
if cancel_event is not None and cancel_event.is_set(): if cancel_event is not None and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Operacja anulowana")) progress_queue.put(("label", _("Operacja anulowana")))
return return
# Prepare tags and rating # Prepare tags and rating
@ -1410,7 +1572,7 @@ class ImageBrowser(tk.Tk):
# Check for cancellation before sending the update request. # Check for cancellation before sending the update request.
if cancel_event is not None and cancel_event.is_set(): if cancel_event is not None and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Operacja anulowana")) progress_queue.put(("label", _("Operacja anulowana")))
return return
# Send update request # Send update request
@ -1420,23 +1582,23 @@ class ImageBrowser(tk.Tk):
if response.status_code == 302: if response.status_code == 302:
if progress_queue: if progress_queue:
progress_queue.put(("progress", 100)) progress_queue.put(("progress", 100))
message = "Tagi zostały zaktualizowane!" message = _("Tagi zostały zaktualizowane!")
if not final_tags: # Only show success if not bulk operation if not final_tags: # Only show success if not bulk operation
messagebox.showinfo("Sukces edycji", message) messagebox.showinfo(_("Sukces edycji"), message)
# Update UI state # Update UI state
self.after(0, self.update_button_states) self.after(0, self.update_button_states)
return return
# Handle other status codes # Handle other status codes
error_msg = ( error_msg = _("Błąd podczas aktualizacji tagów\nStatus: {code}").format(
f"Błąd podczas aktualizacji tagów\nStatus: {response.status_code}" code=response.status_code
) )
if response.text: if response.text:
error_msg += f"\nTreść: {response.text}" error_msg += f"\n{_('Treść:')} {response.text}"
messagebox.showerror("Błąd edycji", error_msg) messagebox.showerror("Błąd edycji", error_msg)
except Exception as e: except Exception as e:
messagebox.showerror("Krytyczny błąd edycji", str(e)) messagebox.showerror(_("Krytyczny błąd edycji"), str(e))
finally: finally:
if progress_queue: if progress_queue:
progress_queue.put(("progress", 100)) progress_queue.put(("progress", 100))
@ -1448,12 +1610,18 @@ class ImageBrowser(tk.Tk):
i wywołuje upload_file. i wywołuje upload_file.
""" """
if not messagebox.askyesno( if not messagebox.askyesno(
"Potwierdzenie", _("Potwierdzenie"),
"Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\nKażdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\nUpewnij się, że tagi są poprawne!", _(
"Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\nKażdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\nUpewnij się, że tagi są poprawne!"
),
): ):
return return
def worker(progress_queue: queue = None, cancel_event: threading.Event = None): def worker(
progress_queue: queue.Queue = None,
cancel_event: threading.Event = None,
secondary_progress_queue: queue.Queue = None,
):
files_count = len(self.image_files) files_count = len(self.image_files)
if progress_queue: if progress_queue:
progress_queue.put(("mode", "determinate")) progress_queue.put(("mode", "determinate"))
@ -1461,26 +1629,37 @@ class ImageBrowser(tk.Tk):
file_idx = 0 file_idx = 0
for file_path in self.image_files: for file_path in self.image_files:
if progress_queue: if progress_queue:
progress_queue.put(("progress", file_idx * 1.0 / files_count)) progress_queue.put(("progress", file_idx * 100 / files_count))
progress_queue.put( progress_queue.put(
("label", f"Wysyłam plik {file_idx+1}/{files_count}...") ("label", f"Wysyłam plik {file_idx+1}/{files_count}...")
) )
if cancel_event is not None and cancel_event.is_set(): if cancel_event is not None and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", f"Anulowano operację!")) progress_queue.put(("label", _("Anulowano operację!")))
return return
if not self.uploaded.get(file_path, False): if not self.uploaded.get(file_path, False):
final_tags, final_rating = ( final_tags, final_rating = (
self.compute_final_tags_and_rating_for_file(file_path) self.compute_final_tags_and_rating_for_file(file_path)
) )
print( print(
f"Uploading {file_path} z tagami: {final_tags} i ratingiem: {final_rating}" _(
"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}"
).format(
file_path=file_path,
final_tags=final_tags,
final_rating=final_rating,
)
) )
self.upload_file( self.upload_file(
file_path, final_tags=final_tags, final_rating=final_rating file_path,
final_tags=final_tags,
final_rating=final_rating,
progress_queue=secondary_progress_queue,
cancel_event=cancel_event,
) )
file_idx += 1
if progress_queue: if progress_queue:
progress_queue.put(("label", f"Przesłano pliki!")) progress_queue.put(("label", _("Przesłano pliki!")))
progress_queue.put(("progress", 100)) progress_queue.put(("progress", 100))
self.processing_dialog = ProcessingDialog(self, worker) self.processing_dialog = ProcessingDialog(self, worker)

View File

@ -1,3 +1,6 @@
from .I18N import _
# Klasa pomocnicza do monitorowania postępu uploadu # Klasa pomocnicza do monitorowania postępu uploadu
class ProgressFile: class ProgressFile:
def __init__(self, f, callback, total_size, cancel_event=None): def __init__(self, f, callback, total_size, cancel_event=None):
@ -10,7 +13,7 @@ class ProgressFile:
def read(self, size=-1): def read(self, size=-1):
# Check for cancellation before reading more data # Check for cancellation before reading more data
if self.cancel_event is not None and self.cancel_event.is_set(): if self.cancel_event is not None and self.cancel_event.is_set():
raise Exception("Upload cancelled by user.") raise Exception(_("Upload cancelled by user."))
data = self.f.read(size) data = self.f.read(size)
self.read_bytes += len(data) self.read_bytes += len(data)

View File

@ -1,11 +1,15 @@
from collections import deque from collections import deque
import json import json
import os import os
import queue
import sqlite3 import sqlite3
import time
import requests
from pathlib import Path from pathlib import Path
import threading
import time
import requests
from .I18N import _
from .settings import Settings from .settings import Settings
# Stałe auth_token (CSRF token) oraz ciasteczka # Stałe auth_token (CSRF token) oraz ciasteczka
@ -51,7 +55,11 @@ class TagsRepo:
regenerate = False regenerate = False
if not Path(self.db_path).is_file(): if not Path(self.db_path).is_file():
regenerate = True regenerate = True
print(f"Database file not found: {self.db_path}, will regenerate DB") print(
_("Database file not found: {path}, will regenerate DB").format(
path=self.db_path
)
)
self.init_tags_db() self.init_tags_db()
if regenerate: if regenerate:
self.regenerate_db() self.regenerate_db()
@ -132,9 +140,11 @@ class TagsRepo:
conn.commit() conn.commit()
conn.close() conn.close()
except Exception as e: except Exception as e:
print("Błąd przy inicjalizacji bazy tagów:", e) print(_("Błąd przy inicjalizacji bazy tagów:"), e)
def regenerate_db(self, progress_queue=None, cancel_event=None): def regenerate_db(
self, progress_queue: queue.Queue = None, cancel_event: threading.Event = None
):
""" """
Regenerate the database of tags, aliases, and tag implications. Regenerate the database of tags, aliases, and tag implications.
@ -146,7 +156,7 @@ class TagsRepo:
cursor = conn.cursor() cursor = conn.cursor()
if progress_queue: if progress_queue:
progress_queue.put(("label", "Czyszczenie bazy danych...")) progress_queue.put(("label", _("Czyszczenie bazy danych...")))
cursor.execute("DELETE FROM tags") cursor.execute("DELETE FROM tags")
cursor.execute("DELETE FROM tag_aliases") cursor.execute("DELETE FROM tag_aliases")
conn.commit() conn.commit()
@ -162,12 +172,19 @@ class TagsRepo:
while True: while True:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Anulowano pobieranie tagów.")) progress_queue.put(("label", _("Anulowano pobieranie tagów.")))
conn.close() conn.close()
return return
if progress_queue: if progress_queue:
progress_queue.put(("label", f"Pobieranie tagów (od ID {last_id})...")) progress_queue.put(
(
"label",
_("Pobieranie tagów (od ID {last_id})...").format(
last_id=last_id
),
)
)
start_time = time.monotonic() start_time = time.monotonic()
url = f"https://danbooru.donmai.us/tags.json?limit=1000&page=a{last_id}" url = f"https://danbooru.donmai.us/tags.json?limit=1000&page=a{last_id}"
response = requests.get(url) response = requests.get(url)
@ -176,7 +193,9 @@ class TagsRepo:
progress_queue.put( progress_queue.put(
( (
"label", "label",
f"Błąd przy pobieraniu tagów od ID {last_id}: HTTP {response.status_code}", _(
"Błąd przy pobieraniu tagów od ID {last_id}: HTTP {code}"
).format(last_id=last_id, code=response.status_code),
) )
) )
break break
@ -189,7 +208,9 @@ class TagsRepo:
for item in data: for item in data:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Anulowano przetwarzanie tagów.")) progress_queue.put(
("label", _("Anulowano przetwarzanie tagów."))
)
conn.close() conn.close()
return return
tag_id = item.get("id") tag_id = item.get("id")
@ -223,7 +244,9 @@ class TagsRepo:
time.sleep(min_interval - elapsed) time.sleep(min_interval - elapsed)
if progress_queue: if progress_queue:
progress_queue.put(("label", f"Pobrano {len(data_list)} tagów...")) progress_queue.put(
("label", _("Pobrano {count} tagów...").format(count=len(data_list)))
)
data_list = sorted(data_list, key=lambda x: x[0]) data_list = sorted(data_list, key=lambda x: x[0])
data_list = [(idx,) + row for idx, row in enumerate(data_list)] data_list = [(idx,) + row for idx, row in enumerate(data_list)]
@ -244,13 +267,20 @@ class TagsRepo:
while True: while True:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put(("label", "Anulowano pobieranie aliasów tagów.")) progress_queue.put(
("label", _("Anulowano pobieranie aliasów tagów."))
)
conn.close() conn.close()
return return
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", f"Pobieranie aliasów tagów (od ID {last_id})...") (
"label",
_("Pobieranie aliasów tagów (od ID {last_id})...").format(
last_id=last_id
),
)
) )
start_time = time.monotonic() start_time = time.monotonic()
url = ( url = (
@ -263,7 +293,9 @@ class TagsRepo:
progress_queue.put( progress_queue.put(
( (
"label", "label",
f"Błąd przy pobieraniu aliasów tagów od ID {last_id}: HTTP {response.status_code}", _(
"Błąd przy pobieraniu aliasów tagów od ID {last_id}: HTTP {code}"
).format(last_id=last_id, code=response.status_code),
) )
) )
break break
@ -277,7 +309,7 @@ class TagsRepo:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", "Anulowano przetwarzanie aliasów tagów.") ("label", _("Anulowano przetwarzanie aliasów tagów."))
) )
conn.close() conn.close()
return return
@ -296,7 +328,12 @@ class TagsRepo:
time.sleep(min_interval - elapsed) time.sleep(min_interval - elapsed)
if progress_queue: if progress_queue:
progress_queue.put(("label", f"Pobrano {len(data_list)} aliasów tagów...")) progress_queue.put(
(
"label",
_("Pobrano {count} aliasów tagów...").format(count=len(data_list)),
)
)
data_list = sorted(data_list, key=lambda x: x[0]) data_list = sorted(data_list, key=lambda x: x[0])
data_list = [(idx,) + row for idx, row in enumerate(data_list)] data_list = [(idx,) + row for idx, row in enumerate(data_list)]
@ -335,14 +372,19 @@ class TagsRepo:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", "Anulowano pobieranie implikacji tagów.") ("label", _("Anulowano pobieranie implikacji tagów."))
) )
conn.close() conn.close()
return return
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", f"Pobieranie implikacji tagów (od ID {last_id})...") (
"label",
_("Pobieranie implikacji tagów (od ID {last_id})...").format(
last_id=last_id
),
)
) )
start_time = time.monotonic() start_time = time.monotonic()
url = f"https://danbooru.donmai.us/tag_implications.json?limit=1000&page=a{last_id}" url = f"https://danbooru.donmai.us/tag_implications.json?limit=1000&page=a{last_id}"
@ -352,7 +394,9 @@ class TagsRepo:
progress_queue.put( progress_queue.put(
( (
"label", "label",
f"Błąd przy pobieraniu implikacji tagów od ID {last_id}: HTTP {response.status_code}", _(
"Błąd przy pobieraniu implikacji tagów od ID {last_id}: HTTP {code}"
).format(last_id=last_id, code=response.status_code),
) )
) )
break break
@ -366,7 +410,7 @@ class TagsRepo:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", "Anulowano przetwarzanie implikacji tagów.") ("label", _("Anulowano przetwarzanie implikacji tagów."))
) )
conn.close() conn.close()
return return
@ -411,7 +455,12 @@ class TagsRepo:
if progress_queue: if progress_queue:
progress_queue.put( progress_queue.put(
("label", f"Pobrano implikacje dla {len(tag_dict)} tagów...") (
"label",
_("Pobrano implikacje dla {count} tagów...").format(
count=len(tag_dict)
),
)
) )
# ----------------------- # -----------------------
@ -435,7 +484,7 @@ class TagsRepo:
conn.close() conn.close()
if progress_queue: if progress_queue:
progress_queue.put(("label", "Regeneracja bazy zakończona.")) progress_queue.put(("label", _("Regeneracja bazy zakończona.")))
def build_transitive_closure(self, tag_dict): def build_transitive_closure(self, tag_dict):
closure = set() closure = set()

View File

@ -1,6 +1,7 @@
import tkinter as tk import tkinter as tk
from tkinter import font from tkinter import font
from .I18N import _
from .TagsRepo import TagsRepo from .TagsRepo import TagsRepo
from .common import open_tag_wiki_url from .common import open_tag_wiki_url
from .tag_processing import process_tag from .tag_processing import process_tag
@ -24,7 +25,37 @@ class AutocompleteEntry(tk.Entry):
self.bind("<Up>", self.on_up) self.bind("<Up>", self.on_up)
self.bind("<Return>", self.on_return) self.bind("<Return>", self.on_return)
self.bind("<Tab>", self.on_return) self.bind("<Tab>", self.on_return)
self.bind("<FocusOut>", lambda e: self.hide_listbox()) self.bind("<FocusOut>", self.on_focus_out)
def on_focus_out(self, event):
self.after(10, self.delayed_focus_check)
def delayed_focus_check(self):
try:
# Get current focus using Tk's internal command
focused_name = self.tk.call("focus")
if not focused_name:
self.hide_listbox()
return
# Convert to widget object
focused_widget = self.nametowidget(focused_name)
# Check if focus is in our listbox hierarchy
if self.listbox_window and self.is_child_of_listbox_window(focused_widget):
return
except (KeyError, tk.TclError):
pass
self.hide_listbox()
def is_child_of_listbox_window(self, widget):
current = widget
while current:
if current == self.listbox_window:
return True
current = current.master
return False
def on_keyrelease(self, event): def on_keyrelease(self, event):
if event.keysym in ("Down", "Up", "Return", "Tab"): if event.keysym in ("Down", "Up", "Return", "Tab"):
@ -99,7 +130,7 @@ class AutocompleteEntry(tk.Entry):
self.suggestion_map[display_text] = tag_insert self.suggestion_map[display_text] = tag_insert
return suggestions return suggestions
except Exception as e: except Exception as e:
print("Błąd przy pobieraniu sugestii:", e) print(_("Błąd przy pobieraniu sugestii:"), e)
return [] return []
def show_listbox(self): def show_listbox(self):
@ -139,12 +170,22 @@ class AutocompleteEntry(tk.Entry):
def on_listbox_click(self, event): def on_listbox_click(self, event):
if self.listbox: if self.listbox:
index = self.listbox.curselection() # Process click first before changing focus
if index: index = self.listbox.nearest(event.y)
value = self.listbox.get(index) if index >= 0:
# Get the display text from listbox
selected_display = self.listbox.get(index)
# Get the actual tag from the suggestion map
tag = self.suggestion_map.get(selected_display, selected_display)
if self.callback:
self.callback(tag)
# Clear the entry and hide listbox
self.delete(0, tk.END) self.delete(0, tk.END)
self.insert(tk.END, value) # Then explicitly manage focus
self.hide_listbox() self.focus_set() # Use focus_set() instead of focus_force()
self.hide_listbox()
return "break" return "break"
def on_listbox_motion(self, event): def on_listbox_motion(self, event):

View File

@ -3,10 +3,11 @@ import subprocess
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import requests import requests
from .I18N import _
from .settings import Settings from .settings import Settings
def open_tag_wiki_url(tag, settings: Settings): def open_tag_wiki_url(tag: str, settings: Settings):
"""Otwiera w przeglądarce URL strony wiki dla podanego tagu.""" """Otwiera w przeglądarce URL strony wiki dla podanego tagu."""
# Usuń prefiksy # Usuń prefiksy
for prefix in [ for prefix in [
@ -31,7 +32,7 @@ def open_webbrowser(url, settings: Settings):
subprocess.run([settings.browser, url], check=True) subprocess.run([settings.browser, url], check=True)
return return
except Exception as e: except Exception as e:
print("Błąd przy otwieraniu przeglądarki:", e) print(_("Błąd przy otwieraniu przeglądarki:"), e)
import webbrowser import webbrowser
webbrowser.open(url) webbrowser.open(url)
@ -68,18 +69,18 @@ def login(settings: Settings):
shm_session = session.cookies.get("shm_session") shm_session = session.cookies.get("shm_session")
if not (shm_user and shm_session): if not (shm_user and shm_session):
raise Exception("Login succeeded, but expected cookies were not set.") raise Exception(_("Login succeeded, but expected cookies were not set."))
print("Login successful. Cookies stored in session:") print(_("Login successful. Cookies stored in session."))
print(f"shm_user: {shm_user}")
print(f"shm_session: {shm_session}")
return session return session
else: else:
raise Exception(f"Login failed: {response.status_code} - {response.text}") raise Exception(
f"{_('Login failed:')} {response.status_code} - {response.text}"
)
def get_auth_token(session, settings): def get_auth_token(session: requests.Session, settings: Settings) -> str:
""" """
Given a logged-in session and settings, fetch the user page Given a logged-in session and settings, fetch the user page
and extract the auth_token from the hidden input field. and extract the auth_token from the hidden input field.
@ -91,7 +92,7 @@ def get_auth_token(session, settings):
# Retrieve the user identifier from cookies # Retrieve the user identifier from cookies
shm_user = session.cookies.get("shm_user") shm_user = session.cookies.get("shm_user")
if not shm_user: if not shm_user:
raise Exception("shm_user cookie not found; login might have failed.") raise Exception(_("shm_user cookie not found; login might have failed."))
# Build the URL to fetch, e.g., /user/<shm_user> # Build the URL to fetch, e.g., /user/<shm_user>
user_url = f"{settings.base_url.rstrip('/')}/user/{shm_user}" user_url = f"{settings.base_url.rstrip('/')}/user/{shm_user}"
@ -108,7 +109,9 @@ def get_auth_token(session, settings):
if response.status_code != 200: if response.status_code != 200:
raise Exception( raise Exception(
f"Failed to load {user_url}, status code: {response.status_code}" _("Failed to load {user_url}, status code: {code}").format(
user_url=user_url, code=response.status_code
)
) )
# Parse the returned HTML with BeautifulSoup # Parse the returned HTML with BeautifulSoup
@ -118,7 +121,7 @@ def get_auth_token(session, settings):
auth_input = soup.find("input", {"name": "auth_token"}) auth_input = soup.find("input", {"name": "auth_token"})
if auth_input and auth_input.has_attr("value"): if auth_input and auth_input.has_attr("value"):
auth_token = auth_input["value"] auth_token = auth_input["value"]
print(f"Found auth_token: {auth_token}") print(_("Found auth_token:"), auth_token)
return auth_token return auth_token
else: else:
raise Exception("auth_token not found in the HTML page.") raise Exception(_("auth_token not found in the HTML page."))

View File

@ -0,0 +1,491 @@
msgid ""
msgstr ""
"Project-Id-Version: Kapitanbooru Uploader 0.4.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:39+0100\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: autocomplete.py:133
msgid "Błąd przy pobieraniu sugestii:"
msgstr "Error fetching suggestions:"
#: common.py:35
msgid "Błąd przy otwieraniu przeglądarki:"
msgstr "Error opening browser:"
#: common.py:72
msgid "Login succeeded, but expected cookies were not set."
msgstr "Login succeeded, but expected cookies were not set."
#: common.py:74
msgid "Login successful. Cookies stored in session."
msgstr "Login successful. Cookies stored in session."
#: common.py:79
msgid "Login failed:"
msgstr "Login failed:"
#: common.py:95
msgid "shm_user cookie not found; login might have failed."
msgstr "shm_user cookie not found; login might have failed."
#: common.py:112
#, python-brace-format
msgid "Failed to load {user_url}, status code: {code}"
msgstr "Failed to load {user_url}, status code: {code}"
#: common.py:124
msgid "Found auth_token:"
msgstr "Found auth_token:"
#: common.py:127
msgid "auth_token not found in the HTML page."
msgstr "auth_token not found in the HTML page."
#: ImageBrowser.py:30
msgid "Processing..."
msgstr "Processing..."
#: ImageBrowser.py:34
msgid "Processing, please wait..."
msgstr "Processing, please wait..."
#: ImageBrowser.py:263
msgid "Błąd przy otwieraniu pliku"
msgstr "Error opening file"
#: ImageBrowser.py:305
msgid "Tagger przetworzył:"
msgstr "Tagger processed:"
#: ImageBrowser.py:308
msgid "Błąd Taggera dla"
msgstr "Tagger error for"
#: ImageBrowser.py:334
msgid "Otwórz folder"
msgstr "Open folder"
#: ImageBrowser.py:337 ImageBrowser.py:600 ImageBrowser.py:814
#: ImageBrowser.py:822
msgid "Wyślij"
msgstr "Send"
#: ImageBrowser.py:340 ImageBrowser.py:807
msgid "Wyślij wszystko"
msgstr "Send all"
#: ImageBrowser.py:344 ImageBrowser.py:808 ImageBrowser.py:814
msgid "Podmień tagi"
msgstr "Replace tags"
#: ImageBrowser.py:347 ImageBrowser.py:809
msgid "Otwórz post"
msgstr "Open post"
#: ImageBrowser.py:350
msgid "Zakończ"
msgstr "Finish"
#: ImageBrowser.py:352
msgid "Plik"
msgstr "File"
#: ImageBrowser.py:356 ImageBrowser.py:430
msgid "Ustawienia"
msgstr "Settings"
#: ImageBrowser.py:359
msgid "Wyczyść cache Taggera"
msgstr "Clear Tagger cache"
#: ImageBrowser.py:362
msgid "Zregeneruj bazę tagów"
msgstr "Regenerate tag database"
#: ImageBrowser.py:364
msgid "Opcje"
msgstr "Options"
#: ImageBrowser.py:367
msgid "About"
msgstr "About"
#: ImageBrowser.py:368
msgid "Help"
msgstr "Help"
#: ImageBrowser.py:373
msgid "About Kapitanbooru Uploader"
msgstr "About Kapitanbooru Uploader"
#: ImageBrowser.py:386
msgid "A GUI application for uploading images to KapitanBooru."
msgstr "A GUI application for uploading images to KapitanBooru."
#: ImageBrowser.py:387
msgid "Features include image upload, tag management, automatic"
msgstr "Features include image upload, tag management, automatic"
#: ImageBrowser.py:388
msgid "tagging with wdtagger, and cache management."
msgstr "tagging with wdtagger, and cache management."
#: ImageBrowser.py:390
msgid "Authors:"
msgstr "Authors:"
#: ImageBrowser.py:393
msgid "License: MIT License"
msgstr "License: MIT License"
#: ImageBrowser.py:396
msgid "Repository:"
msgstr "Repository:"
#: ImageBrowser.py:397
msgid "Website:"
msgstr "Website:"
#: ImageBrowser.py:414
msgid "Close"
msgstr "Close"
#: ImageBrowser.py:422 ImageBrowser.py:425
msgid "Cache"
msgstr "Cache"
#: ImageBrowser.py:422
msgid "Cache Taggera zostało wyczyszczone."
msgstr "Tagger cache has been cleared."
#: ImageBrowser.py:425
msgid "Błąd przy czyszczeniu cache:"
msgstr "Error clearing cache:"
#: ImageBrowser.py:434
msgid "Login:"
msgstr "Login:"
#: ImageBrowser.py:440
msgid "Hasło:"
msgstr "Password:"
#: ImageBrowser.py:446
msgid "Base URL:"
msgstr "Base URL:"
#: ImageBrowser.py:452
msgid "Default Tags:"
msgstr "Default Tags:"
#: ImageBrowser.py:458
msgid "Browser:"
msgstr "Browser:"
#: ImageBrowser.py:472
msgid "Language:"
msgstr "Language:"
#: ImageBrowser.py:504
msgid "Zapisz"
msgstr "Save"
#: ImageBrowser.py:546
msgid "PNG Tags"
msgstr "PNG Tags"
#: ImageBrowser.py:558
msgid "Tagger Tags"
msgstr "Tagger Tags"
#: ImageBrowser.py:572
msgid "Manual Tags"
msgstr "Manual Tags"
#: ImageBrowser.py:580
msgid "Final Tags"
msgstr "Final Tags"
#: ImageBrowser.py:605
msgid "Wyświetl"
msgstr "Display"
#: ImageBrowser.py:622
msgid "Przetworzono tagi:"
msgstr "Processed tags:"
#: ImageBrowser.py:622 ImageBrowser.py:623 ImageBrowser.py:624
msgid "plików"
msgstr "files"
#: ImageBrowser.py:623
msgid "Zweryfikowano status uploadu:"
msgstr "Upload status verified:"
#: ImageBrowser.py:624
msgid "Zuploadowano:"
msgstr "Uploaded:"
#: ImageBrowser.py:652
msgid "Wybierz folder z obrazkami PNG"
msgstr "Select folder with PNG images"
#: ImageBrowser.py:683
msgid "Informacja"
msgstr "Information"
#: ImageBrowser.py:683
msgid "Brak plików PNG w wybranym folderze."
msgstr "No PNG files in the selected folder."
#: ImageBrowser.py:766
msgid "Błąd podczas sprawdzania paczki uploadu:"
msgstr "Error while checking upload package:"
#: ImageBrowser.py:842
msgid "Błąd przy obliczaniu MD5:"
msgstr "Error calculating MD5:"
#: ImageBrowser.py:898
msgid "Błąd"
msgstr "Error"
#: ImageBrowser.py:898
msgid "Nie można załadować obrazka:"
msgstr "Unable to load image:"
#: ImageBrowser.py:1108 ImageBrowser.py:1118
#, python-brace-format
msgid "Warning: Tag '{tag}' not found in implication graph"
msgstr "Warning: Tag '{tag}' not found in implication graph"
#: ImageBrowser.py:1348
msgid "Tagger przetwarza..."
msgstr "Tagger processing..."
#: ImageBrowser.py:1373
#, python-brace-format
msgid "Wysyłam plik {base_file_name}..."
msgstr "Sending file {base_file_name}..."
#: ImageBrowser.py:1414
msgid "Wysyłanie zakończone powodzeniem!"
msgstr "Upload completed successfully!"
#: ImageBrowser.py:1418 ImageBrowser.py:1427
#, python-brace-format
msgid ""
"Wysyłanie zakończone błędem.\n"
"Status: {status_code}\n"
"Treść: {text}"
msgstr ""
"Upload failed.\nStatus: {status_code}\nContent: {text}"
#: ImageBrowser.py:1433 ImageBrowser.py:1436
msgid "Wysyłanie"
msgstr "Uploading"
#: ImageBrowser.py:1449
msgid "Błąd wysyłania"
msgstr "Upload error"
#: ImageBrowser.py:1469
msgid "Błąd edycji"
msgstr "Edit error"
#: ImageBrowser.py:1469
msgid "Post nie został znaleziony dla tego pliku"
msgstr "Post not found for this file"
#: ImageBrowser.py:1479
#, python-brace-format
msgid "Aktualizuję tagi dla {base_file_name}..."
msgstr "Updating tags for {base_file_name}..."
#: ImageBrowser.py:1489 ImageBrowser.py:1499 ImageBrowser.py:1530
msgid "Operacja anulowana"
msgstr "Operation cancelled"
#: ImageBrowser.py:1540
msgid "Tagi zostały zaktualizowane!"
msgstr "Tags have been updated!"
#: ImageBrowser.py:1542
msgid "Sukces edycji"
msgstr "Edit successful"
#: ImageBrowser.py:1548
#, python-brace-format
msgid ""
"Błąd podczas aktualizacji tagów\n"
"Status: {code}"
msgstr ""
"Error updating tags\nStatus: {code}"
#: ImageBrowser.py:1552
msgid "Treść:"
msgstr "Content:"
#: ImageBrowser.py:1556
msgid "Krytyczny błąd edycji"
msgstr "Critical edit error"
#: ImageBrowser.py:1568
msgid "Potwierdzenie"
msgstr "Confirmation"
#: ImageBrowser.py:1570
msgid ""
"Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\n"
"Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n"
"Upewnij się, że tagi są poprawne!"
msgstr ""
"Are you sure you want to upload all unsubmitted files?\n"
"Each will be tagged with 'meta:auto_upload'.\n"
"Make sure the tags are correct!"
#: ImageBrowser.py:1589
msgid "Anulowano operację!"
msgstr "Operation cancelled!"
#: ImageBrowser.py:1597
#, python-brace-format
msgid ""
"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}"
msgstr "Uploading {file_path} with tags: {final_tags} and rating: {final_rating}"
#: ImageBrowser.py:1608
msgid "Przesłano pliki!"
msgstr "Files have been uploaded!"
#: ProgressFile.py:16
msgid "Upload cancelled by user."
msgstr "Upload cancelled by user."
#: settings.py:163
msgid "Błąd podczas ładowania ustawień:"
msgstr "Error loading settings:"
#: settings.py:184
msgid "Błąd podczas zapisywania ustawień:"
msgstr "Error saving settings:"
#: tagger_cache.py:63
msgid "Błąd przy odczycie cache dla"
msgstr "Error reading cache for"
#: tagger_cache.py:88
msgid "Błąd przy zapisie cache dla"
msgstr "Error writing cache for"
#: tagger_cache.py:98
msgid "Błąd przy usuwaniu cache dla"
msgstr "Error deleting cache for"
#: tagger_cache.py:111
msgid "Błąd przy czyszczeniu przeterminowanego cache:"
msgstr "Error clearing expired cache:"
#: TagsRepo.py:59
#, python-brace-format
msgid "Database file not found: {path}, will regenerate DB"
msgstr "Database file not found: {path}, will regenerate DB"
#: TagsRepo.py:143
msgid "Błąd przy inicjalizacji bazy tagów:"
msgstr "Error initializing tag database:"
#: TagsRepo.py:159
msgid "Czyszczenie bazy danych..."
msgstr "Cleaning database..."
#: TagsRepo.py:175
msgid "Anulowano pobieranie tagów."
msgstr "Tag fetching cancelled."
#: TagsRepo.py:183
#, python-brace-format
msgid "Pobieranie tagów (od ID {last_id})..."
msgstr "Fetching tags (from ID {last_id})..."
#: TagsRepo.py:197
#, python-brace-format
msgid "Błąd przy pobieraniu tagów od ID {last_id}: HTTP {code}"
msgstr "Error fetching tags from ID {last_id}: HTTP {code}"
#: TagsRepo.py:212
msgid "Anulowano przetwarzanie tagów."
msgstr "Tag processing cancelled."
#: TagsRepo.py:248
#, python-brace-format
msgid "Pobrano {count} tagów..."
msgstr "Fetched {count} tags..."
#: TagsRepo.py:271
msgid "Anulowano pobieranie aliasów tagów."
msgstr "Tag alias fetching cancelled."
#: TagsRepo.py:280
#, python-brace-format
msgid "Pobieranie aliasów tagów (od ID {last_id})..."
msgstr "Fetching tag aliases (from ID {last_id})..."
#: TagsRepo.py:297
#, python-brace-format
msgid "Błąd przy pobieraniu aliasów tagów od ID {last_id}: HTTP {code}"
msgstr "Error fetching tag aliases from ID {last_id}: HTTP {code}"
#: TagsRepo.py:312
msgid "Anulowano przetwarzanie aliasów tagów."
msgstr "Tag alias processing cancelled."
#: TagsRepo.py:334
#, python-brace-format
msgid "Pobrano {count} aliasów tagów..."
msgstr "Fetched {count} tag aliases..."
#: TagsRepo.py:375
msgid "Anulowano pobieranie implikacji tagów."
msgstr "Tag implication fetching cancelled."
#: TagsRepo.py:384
#, python-brace-format
msgid "Pobieranie implikacji tagów (od ID {last_id})..."
msgstr "Fetching tag implications (from ID {last_id})..."
#: TagsRepo.py:398
#, python-brace-format
msgid "Błąd przy pobieraniu implikacji tagów od ID {last_id}: HTTP {code}"
msgstr "Error fetching tag implications from ID {last_id}: HTTP {code}"
#: TagsRepo.py:413
msgid "Anulowano przetwarzanie implikacji tagów."
msgstr "Tag implication processing cancelled."
#: TagsRepo.py:460
#, python-brace-format
msgid "Pobrano implikacje dla {count} tagów..."
msgstr "Fetched implications for {count} tags..."
#: TagsRepo.py:487
msgid "Regeneracja bazy zakończona."
msgstr "Database regeneration complete."
#: tag_processing.py:19
msgid "Błąd przy pobieraniu tagów postaci:"
msgstr "Error fetching character tags:"
#: tag_processing.py:34
msgid "Błąd przy pobieraniu tagów copyright:"
msgstr "Error fetching copyright tags:"
#: tag_processing.py:165
msgid "Błąd podczas odczytu tag_aliases:"
msgstr "Error reading tag aliases:"
#: tag_processing.py:181
msgid "Błąd podczas odczytu tags:"
msgstr "Error reading tags:"

View File

@ -0,0 +1,515 @@
msgid ""
msgstr ""
"Project-Id-Version: Kapitanbooru Uploader 0.4.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:39+0100\n"
"Language: pl\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: autocomplete.py:133
msgid "Błąd przy pobieraniu sugestii:"
msgstr "Błąd przy pobieraniu sugestii:"
#: common.py:35
msgid "Błąd przy otwieraniu przeglądarki:"
msgstr "Błąd przy otwieraniu przeglądarki:"
#: common.py:72
msgid "Login succeeded, but expected cookies were not set."
msgstr ""
"Logowanie powiodło się, ale oczekiwane ciasteczka nie zostały ustawione."
#: common.py:74
msgid "Login successful. Cookies stored in session."
msgstr "Logowanie powiodło się. Ciasteczka zapisane w sesji."
#: common.py:79
msgid "Login failed:"
msgstr "Logowanie nie powiodło się:"
#: common.py:95
msgid "shm_user cookie not found; login might have failed."
msgstr "Nie znaleziono ciasteczka shm_user; logowanie mogło nie powieść się."
#: common.py:112
#, python-brace-format
msgid "Failed to load {user_url}, status code: {code}"
msgstr "Nie udało się załadować {user_url}, kod statusu: {code}"
#: common.py:124
msgid "Found auth_token:"
msgstr "Znaleziono auth_token:"
#: common.py:127
msgid "auth_token not found in the HTML page."
msgstr "Nie znaleziono auth_token w stronie HTML."
#: ImageBrowser.py:30
msgid "Processing..."
msgstr "Przetwarzanie..."
#: ImageBrowser.py:34
msgid "Processing, please wait..."
msgstr "Przetwarzanie, proszę czekać..."
#: ImageBrowser.py:263
msgid "Błąd przy otwieraniu pliku"
msgstr "Błąd przy otwieraniu pliku"
#: ImageBrowser.py:305
msgid "Tagger przetworzył:"
msgstr "Tagger przetworzył:"
#: ImageBrowser.py:308
msgid "Błąd Taggera dla"
msgstr "Błąd Taggera dla"
#: ImageBrowser.py:334
msgid "Otwórz folder"
msgstr "Otwórz folder"
#: ImageBrowser.py:337 ImageBrowser.py:600 ImageBrowser.py:814
#: ImageBrowser.py:822
msgid "Wyślij"
msgstr "Wyślij"
#: ImageBrowser.py:340 ImageBrowser.py:807
msgid "Wyślij wszystko"
msgstr "Wyślij wszystko"
#: ImageBrowser.py:344 ImageBrowser.py:808 ImageBrowser.py:814
msgid "Podmień tagi"
msgstr "Podmień tagi"
#: ImageBrowser.py:347 ImageBrowser.py:809
msgid "Otwórz post"
msgstr "Otwórz post"
#: ImageBrowser.py:350
msgid "Zakończ"
msgstr "Zakończ"
#: ImageBrowser.py:352
msgid "Plik"
msgstr "Plik"
#: ImageBrowser.py:356 ImageBrowser.py:430
msgid "Ustawienia"
msgstr "Ustawienia"
#: ImageBrowser.py:359
msgid "Wyczyść cache Taggera"
msgstr "Wyczyść cache Taggera"
#: ImageBrowser.py:362
msgid "Zregeneruj bazę tagów"
msgstr "Zregeneruj bazę tagów"
#: ImageBrowser.py:364
msgid "Opcje"
msgstr "Opcje"
#: ImageBrowser.py:367
msgid "About"
msgstr "O programie"
#: ImageBrowser.py:368
msgid "Help"
msgstr "Pomoc"
#: ImageBrowser.py:373
msgid "About Kapitanbooru Uploader"
msgstr "O programie Kapitanbooru Uploader"
#: ImageBrowser.py:386
msgid "A GUI application for uploading images to KapitanBooru."
msgstr "Aplikacja GUI do przesyłania obrazów do KapitanBooru."
#: ImageBrowser.py:387
msgid "Features include image upload, tag management, automatic"
msgstr "Funkcje obejmują przesyłanie obrazów, zarządzanie tagami, automatyczne"
#: ImageBrowser.py:388
msgid "tagging with wdtagger, and cache management."
msgstr "tagowanie za pomocą wdtagger oraz zarządzanie cache."
#: ImageBrowser.py:390
msgid "Authors:"
msgstr "Autorzy:"
#: ImageBrowser.py:393
msgid "License: MIT License"
msgstr "Licencja: MIT License"
#: ImageBrowser.py:396
msgid "Repository:"
msgstr "Repozytorium:"
#: ImageBrowser.py:397
msgid "Website:"
msgstr "Strona internetowa:"
#: ImageBrowser.py:414
msgid "Close"
msgstr "Zamknij"
#: ImageBrowser.py:422 ImageBrowser.py:425
msgid "Cache"
msgstr "Cache"
#: ImageBrowser.py:422
msgid "Cache Taggera zostało wyczyszczone."
msgstr "Cache Taggera zostało wyczyszczone."
#: ImageBrowser.py:425
msgid "Błąd przy czyszczeniu cache:"
msgstr "Błąd przy czyszczeniu cache:"
#: ImageBrowser.py:434
msgid "Login:"
msgstr "Login:"
#: ImageBrowser.py:440
msgid "Hasło:"
msgstr "Hasło:"
#: ImageBrowser.py:446
msgid "Base URL:"
msgstr "Base URL:"
#: ImageBrowser.py:452
msgid "Default Tags:"
msgstr "Domyślne tagi:"
#: ImageBrowser.py:458
msgid "Browser:"
msgstr "Przeglądarka:"
#: ImageBrowser.py:472
msgid "Language:"
msgstr "Język:"
#: ImageBrowser.py:504
msgid "Zapisz"
msgstr "Zapisz"
#: ImageBrowser.py:546
msgid "PNG Tags"
msgstr "Tagi PNG"
#: ImageBrowser.py:558
msgid "Tagger Tags"
msgstr "Tagi Taggera"
#: ImageBrowser.py:572
msgid "Manual Tags"
msgstr "Tagi ręczne"
#: ImageBrowser.py:580
msgid "Final Tags"
msgstr "Ostateczne tagi"
#: ImageBrowser.py:605
msgid "Wyświetl"
msgstr "Wyświetl"
#: ImageBrowser.py:622
msgid "Przetworzono tagi:"
msgstr "Przetworzono tagi:"
#: ImageBrowser.py:622 ImageBrowser.py:623 ImageBrowser.py:624
msgid "plików"
msgstr "plików"
#: ImageBrowser.py:623
msgid "Zweryfikowano status uploadu:"
msgstr "Zweryfikowano status uploadu:"
#: ImageBrowser.py:624
msgid "Zuploadowano:"
msgstr "Zuploadowano:"
#: ImageBrowser.py:652
msgid "Wybierz folder z obrazkami PNG"
msgstr "Wybierz folder z obrazami PNG"
#: ImageBrowser.py:683
msgid "Informacja"
msgstr "Informacja"
#: ImageBrowser.py:683
msgid "Brak plików PNG w wybranym folderze."
msgstr "Brak plików PNG w wybranym folderze."
#: ImageBrowser.py:766
msgid "Błąd podczas sprawdzania paczki uploadu:"
msgstr "Błąd podczas sprawdzania paczki uploadu:"
#: ImageBrowser.py:842
msgid "Błąd przy obliczaniu MD5:"
msgstr "Błąd przy obliczaniu MD5:"
#: ImageBrowser.py:898
msgid "Błąd"
msgstr "Błąd"
#: ImageBrowser.py:898
msgid "Nie można załadować obrazka:"
msgstr "Nie można załadować obrazka:"
#: ImageBrowser.py:1108 ImageBrowser.py:1118
#, python-brace-format
msgid "Warning: Tag '{tag}' not found in implication graph"
msgstr "Ostrzeżenie: Tag '{tag}' nie został znaleziony w grafie implikacji"
#: ImageBrowser.py:1348
msgid "Tagger przetwarza..."
msgstr "Tagger przetwarza..."
#: ImageBrowser.py:1373
#, python-brace-format
msgid "Wysyłam plik {base_file_name}..."
msgstr "Wysyłam plik {base_file_name}..."
#: ImageBrowser.py:1414
#, fuzzy
msgid "Wysyłanie zakończone powodzeniem!"
msgstr "Upload zakończony powodzeniem!"
#: ImageBrowser.py:1418 ImageBrowser.py:1427
#, fuzzy, python-brace-format
msgid ""
"Wysyłanie zakończone błędem.\n"
"Status: {status_code}\n"
"Treść: {text}"
msgstr ""
"Upload zakończony błędem.\n"
"Status: {status_code}\n"
"Treść: {text}"
#: ImageBrowser.py:1433 ImageBrowser.py:1436
msgid "Wysyłanie"
msgstr ""
#: ImageBrowser.py:1449
#, fuzzy
msgid "Błąd wysyłania"
msgstr "Błąd edycji"
#: ImageBrowser.py:1469
msgid "Błąd edycji"
msgstr "Błąd edycji"
#: ImageBrowser.py:1469
msgid "Post nie został znaleziony dla tego pliku"
msgstr "Post nie został znaleziony dla tego pliku"
#: ImageBrowser.py:1479
#, python-brace-format
msgid "Aktualizuję tagi dla {base_file_name}..."
msgstr "Aktualizuję tagi dla {base_file_name}..."
#: ImageBrowser.py:1489 ImageBrowser.py:1499 ImageBrowser.py:1530
msgid "Operacja anulowana"
msgstr "Operacja anulowana"
#: ImageBrowser.py:1540
msgid "Tagi zostały zaktualizowane!"
msgstr "Tagi zostały zaktualizowane!"
#: ImageBrowser.py:1542
msgid "Sukces edycji"
msgstr "Sukces edycji"
#: ImageBrowser.py:1548
#, python-brace-format
msgid ""
"Błąd podczas aktualizacji tagów\n"
"Status: {code}"
msgstr ""
"Błąd podczas aktualizacji tagów\n"
"Status: {code}"
#: ImageBrowser.py:1552
msgid "Treść:"
msgstr "Treść:"
#: ImageBrowser.py:1556
msgid "Krytyczny błąd edycji"
msgstr "Krytyczny błąd edycji"
#: ImageBrowser.py:1568
msgid "Potwierdzenie"
msgstr "Potwierdzenie"
#: ImageBrowser.py:1570
msgid ""
"Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\n"
"Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n"
"Upewnij się, że tagi są poprawne!"
msgstr ""
"Czy na pewno chcesz wrzucić wszystkie niewrzucone pliki?\n"
"Każdy z nich zostanie oznaczony tagiem 'meta:auto_upload'.\n"
"Upewnij się, że tagi są poprawne!"
#: ImageBrowser.py:1589
msgid "Anulowano operację!"
msgstr "Operacja anulowana!"
#: ImageBrowser.py:1597
#, fuzzy, python-brace-format
msgid ""
"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}"
msgstr ""
"Wysyłanie {file_path} z tagami: {final_tags} i ratingiem: {final_rating}"
#: ImageBrowser.py:1608
msgid "Przesłano pliki!"
msgstr "Pliki zostały przesłane!"
#: ProgressFile.py:16
msgid "Upload cancelled by user."
msgstr "Przesyłanie anulowane przez użytkownika."
#: settings.py:163
msgid "Błąd podczas ładowania ustawień:"
msgstr "Błąd podczas ładowania ustawień:"
#: settings.py:184
msgid "Błąd podczas zapisywania ustawień:"
msgstr "Błąd podczas zapisywania ustawień:"
#: tagger_cache.py:63
msgid "Błąd przy odczycie cache dla"
msgstr "Błąd przy odczycie cache dla"
#: tagger_cache.py:88
msgid "Błąd przy zapisie cache dla"
msgstr "Błąd przy zapisie cache dla"
#: tagger_cache.py:98
msgid "Błąd przy usuwaniu cache dla"
msgstr "Błąd przy usuwaniu cache dla"
#: tagger_cache.py:111
msgid "Błąd przy czyszczeniu przeterminowanego cache:"
msgstr "Błąd przy czyszczeniu przeterminowanego cache:"
#: TagsRepo.py:59
#, python-brace-format
msgid "Database file not found: {path}, will regenerate DB"
msgstr "Nie znaleziono pliku bazy danych: {path}, baza zostanie zregenerowana"
#: TagsRepo.py:143
msgid "Błąd przy inicjalizacji bazy tagów:"
msgstr "Błąd przy inicjalizacji bazy tagów:"
#: TagsRepo.py:159
msgid "Czyszczenie bazy danych..."
msgstr "Czyszczenie bazy danych..."
#: TagsRepo.py:175
msgid "Anulowano pobieranie tagów."
msgstr "Pobieranie tagów anulowane."
#: TagsRepo.py:183
#, python-brace-format
msgid "Pobieranie tagów (od ID {last_id})..."
msgstr "Pobieranie tagów (od ID {last_id})..."
#: TagsRepo.py:197
#, python-brace-format
msgid "Błąd przy pobieraniu tagów od ID {last_id}: HTTP {code}"
msgstr "Błąd przy pobieraniu tagów od ID {last_id}: HTTP {code}"
#: TagsRepo.py:212
msgid "Anulowano przetwarzanie tagów."
msgstr "Przetwarzanie tagów anulowane."
#: TagsRepo.py:248
#, python-brace-format
msgid "Pobrano {count} tagów..."
msgstr "Pobrano {count} tagów..."
#: TagsRepo.py:271
msgid "Anulowano pobieranie aliasów tagów."
msgstr "Pobieranie aliasów tagów anulowane."
#: TagsRepo.py:280
#, python-brace-format
msgid "Pobieranie aliasów tagów (od ID {last_id})..."
msgstr "Pobieranie aliasów tagów (od ID {last_id})..."
#: TagsRepo.py:297
#, python-brace-format
msgid "Błąd przy pobieraniu aliasów tagów od ID {last_id}: HTTP {code}"
msgstr "Błąd przy pobieraniu aliasów tagów od ID {last_id}: HTTP {code}"
#: TagsRepo.py:312
msgid "Anulowano przetwarzanie aliasów tagów."
msgstr "Przetwarzanie aliasów tagów anulowane."
#: TagsRepo.py:334
#, python-brace-format
msgid "Pobrano {count} aliasów tagów..."
msgstr "Pobrano {count} aliasów tagów..."
#: TagsRepo.py:375
msgid "Anulowano pobieranie implikacji tagów."
msgstr "Pobieranie implikacji tagów anulowane."
#: TagsRepo.py:384
#, python-brace-format
msgid "Pobieranie implikacji tagów (od ID {last_id})..."
msgstr "Pobieranie implikacji tagów (od ID {last_id})..."
#: TagsRepo.py:398
#, python-brace-format
msgid "Błąd przy pobieraniu implikacji tagów od ID {last_id}: HTTP {code}"
msgstr "Błąd przy pobieraniu implikacji tagów od ID {last_id}: HTTP {code}"
#: TagsRepo.py:413
msgid "Anulowano przetwarzanie implikacji tagów."
msgstr "Przetwarzanie implikacji tagów anulowane."
#: TagsRepo.py:460
#, python-brace-format
msgid "Pobrano implikacje dla {count} tagów..."
msgstr "Pobrano implikacje dla {count} tagów..."
#: TagsRepo.py:487
msgid "Regeneracja bazy zakończona."
msgstr "Regeneracja bazy zakończona."
#: tag_processing.py:19
msgid "Błąd przy pobieraniu tagów postaci:"
msgstr "Błąd przy pobieraniu tagów postaci:"
#: tag_processing.py:34
msgid "Błąd przy pobieraniu tagów copyright:"
msgstr "Błąd przy pobieraniu tagów copyright:"
#: tag_processing.py:165
msgid "Błąd podczas odczytu tag_aliases:"
msgstr "Błąd podczas odczytu tag_aliases:"
#: tag_processing.py:181
msgid "Błąd podczas odczytu tags:"
msgstr "Błąd podczas odczytu tags:"
#, python-brace-format
#~ msgid ""
#~ "Upload zakończony błędem.\n"
#~ "Status: 409\n"
#~ "Treść: {error}"
#~ msgstr ""
#~ "Upload zakończony błędem.\n"
#~ "Status: 409\n"
#~ "Treść: {error}"
#~ msgid "Upload"
#~ msgstr "Upload"
#~ msgid "Błąd uploadu"
#~ msgstr "Błąd uploadu"

View File

@ -1,11 +1,11 @@
import base64 import base64
import importlib
import json import json
import os import os
import sqlite3
import subprocess import subprocess
import sys import sys
from .I18N import _
# Na Windowsie używamy DPAPI # Na Windowsie używamy DPAPI
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
try: try:
@ -49,6 +49,7 @@ def get_browser_paths_windows():
return browsers return browsers
def get_browsers_linux(): def get_browsers_linux():
"""Detects installed browsers on Linux by checking available executables.""" """Detects installed browsers on Linux by checking available executables."""
browsers = {"Default": None} browsers = {"Default": None}
@ -114,6 +115,7 @@ class Settings:
self.default_tags = "artist:kapitan meta:ai-generated" self.default_tags = "artist:kapitan meta:ai-generated"
self.cache_expiry = 604800 # 7 dni w sekundach self.cache_expiry = 604800 # 7 dni w sekundach
self.browser = "" self.browser = ""
self.i18n = _
self.installed_browsers = detect_installed_browsers() self.installed_browsers = detect_installed_browsers()
self.load_settings() self.load_settings()
self.installed_browsers_reverse = { self.installed_browsers_reverse = {
@ -139,6 +141,7 @@ class Settings:
self.default_tags = "artist:kapitan meta:ai-generated" self.default_tags = "artist:kapitan meta:ai-generated"
self.cache_expiry = 604800 # 7 dni w sekundach self.cache_expiry = 604800 # 7 dni w sekundach
self.browser = "" self.browser = ""
self.i18n.set_language("en")
try: try:
if os.path.exists(self.get_settings_path()): if os.path.exists(self.get_settings_path()):
with open(self.get_settings_path(), "r", encoding="utf-8") as f: with open(self.get_settings_path(), "r", encoding="utf-8") as f:
@ -155,8 +158,9 @@ class Settings:
self.browser = data.get("browser", self.browser) self.browser = data.get("browser", self.browser)
if self.browser not in self.installed_browsers: if self.browser not in self.installed_browsers:
self.browser = "" self.browser = ""
self.i18n.set_language(data.get("language", "en"))
except Exception as e: except Exception as e:
print("Błąd podczas ładowania ustawień:", e) print(_("Błąd podczas ładowania ustawień:"), e)
def save_settings(self): def save_settings(self):
"""Zapisuje ustawienia do pliku.""" """Zapisuje ustawienia do pliku."""
@ -166,6 +170,7 @@ class Settings:
"default_tags": self.default_tags, "default_tags": self.default_tags,
"cache_expiry": self.cache_expiry, "cache_expiry": self.cache_expiry,
"browser": self.browser, "browser": self.browser,
"language": self.i18n.current_lang,
} }
# Na Windowsie szyfrujemy hasło # Na Windowsie szyfrujemy hasło
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@ -176,4 +181,4 @@ class Settings:
with open(self.get_settings_path(), "w", encoding="utf-8") as f: with open(self.get_settings_path(), "w", encoding="utf-8") as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
except Exception as e: except Exception as e:
print("Błąd podczas zapisywania ustawień:", e) print(_("Błąd podczas zapisywania ustawień:"), e)

View File

@ -1,6 +1,7 @@
from functools import lru_cache from functools import lru_cache
import re import re
from .I18N import _
from .TagsRepo import TagsRepo from .TagsRepo import TagsRepo
@ -15,7 +16,7 @@ def get_character_tags(tags_repo: TagsRepo):
conn.close() conn.close()
return {row[0] for row in rows} return {row[0] for row in rows}
except Exception as e: except Exception as e:
print("Błąd przy pobieraniu tagów postaci:", e) print(_("Błąd przy pobieraniu tagów postaci:"), e)
return set() return set()
@ -30,7 +31,7 @@ def get_copyright_tags(tags_repo: TagsRepo):
conn.close() conn.close()
return {row[0] for row in rows} return {row[0] for row in rows}
except Exception as e: except Exception as e:
print("Błąd przy pobieraniu tagów copyright:", e) print(_("Błąd przy pobieraniu tagów copyright:"), e)
return set() return set()
@ -161,7 +162,7 @@ def process_tag(tag, tags_repo: TagsRepo):
tag_lookup = row[0] tag_lookup = row[0]
conn.close() conn.close()
except Exception as e: except Exception as e:
print("Błąd podczas odczytu tag_aliases:", e) print(_("Błąd podczas odczytu tag_aliases:"), e)
# Sprawdź w tabeli tags kolumna name nie zawiera prefiksów # Sprawdź w tabeli tags kolumna name nie zawiera prefiksów
try: try:
@ -177,5 +178,5 @@ def process_tag(tag, tags_repo: TagsRepo):
# Tag nie istnieje # Tag nie istnieje
return tag_lookup, None return tag_lookup, None
except Exception as e: except Exception as e:
print("Błąd podczas odczytu tags:", e) print(_("Błąd podczas odczytu tags:"), e)
return tag_lookup, None return tag_lookup, None

View File

@ -2,6 +2,7 @@ import os
import pickle import pickle
import sqlite3 import sqlite3
import time import time
from .I18N import _
from .settings import Settings from .settings import Settings
@ -59,7 +60,7 @@ class TaggerCache:
else: else:
self.delete_cache_entry(file_md5) self.delete_cache_entry(file_md5)
except Exception as e: except Exception as e:
print("Błąd przy odczycie cache dla", file_md5, ":", e) print(_("Błąd przy odczycie cache dla"), file_md5, ":", e)
return None return None
def __setitem__(self, file_md5, result): def __setitem__(self, file_md5, result):
@ -84,7 +85,7 @@ class TaggerCache:
conn.commit() conn.commit()
conn.close() conn.close()
except Exception as e: except Exception as e:
print("Błąd przy zapisie cache dla", file_md5, ":", e) print(_("Błąd przy zapisie cache dla"), file_md5, ":", e)
def delete_cache_entry(self, file_md5): def delete_cache_entry(self, file_md5):
try: try:
@ -94,7 +95,7 @@ class TaggerCache:
conn.commit() conn.commit()
conn.close() conn.close()
except Exception as e: except Exception as e:
print("Błąd przy usuwaniu cache dla", file_md5, ":", e) print(_("Błąd przy usuwaniu cache dla"), file_md5, ":", e)
def clear_expired_cache(self): def clear_expired_cache(self):
try: try:
@ -107,7 +108,7 @@ class TaggerCache:
conn.commit() conn.commit()
conn.close() conn.close()
except Exception as e: except Exception as e:
print("Błąd przy czyszczeniu przeterminowanego cache:", e) print(_("Błąd przy czyszczeniu przeterminowanego cache:"), e)
def clear_cache(self): def clear_cache(self):
try: try:

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kapitanbooru-uploader" name = "kapitanbooru-uploader"
version = "0.3.0" version = "0.4.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"}
@ -25,4 +25,8 @@ license = {file = "LICENSE"}
kapitanbooru-uploader = "kapitanbooru_uploader.__main__:main" kapitanbooru-uploader = "kapitanbooru_uploader.__main__:main"
[tool.setuptools] [tool.setuptools]
packages = ["kapitanbooru_uploader"] packages = ["kapitanbooru_uploader"]
include-package-data = true
[tool.setuptools.package-data]
"kapitanbooru_uploader" = ["locales/*/LC_MESSAGES/*.mo"]

19
update_translations.bat Normal file
View File

@ -0,0 +1,19 @@
@echo off
setlocal enabledelayedexpansion
:: Create POT file
del locales\messages.pot 2>nul
for %%f in (*.py) do (
if not exist locales\messages.pot (
xgettext -d messages -o locales\messages.pot "%%f"
) else (
xgettext -d messages -o locales\messages.pot --join-existing "%%f"
)
)
:: Update PO files
for /D %%d in (locales\*) do (
if exist "%%d\LC_MESSAGES\messages.po" (
msgmerge --update "%%d\LC_MESSAGES\messages.po" locales\messages.pot
)
)