QoL
This commit is contained in:
383
gui.py
383
gui.py
@@ -10,6 +10,7 @@ import time
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
def normalize_file_path(path_str: str) -> str:
|
def normalize_file_path(path_str: str) -> str:
|
||||||
if not path_str:
|
if not path_str:
|
||||||
@@ -28,7 +29,7 @@ from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMulti
|
|||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
|
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox,
|
QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox,
|
||||||
QFormLayout, QMessageBox, QFileDialog
|
QFormLayout, QMessageBox, QFileDialog, QSlider
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configuration paths
|
# Configuration paths
|
||||||
@@ -215,7 +216,7 @@ class SettingsDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.tray_app = tray_app
|
self.tray_app = tray_app
|
||||||
self.setWindowTitle("f0ckm Uploader Settings")
|
self.setWindowTitle("f0ckm Uploader Settings")
|
||||||
self.resize(520, 560)
|
self.resize(540, 680)
|
||||||
|
|
||||||
app_icon = get_app_icon()
|
app_icon = get_app_icon()
|
||||||
if not app_icon.isNull():
|
if not app_icon.isNull():
|
||||||
@@ -331,6 +332,45 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
main_layout.addWidget(grp_behavior)
|
main_layout.addWidget(grp_behavior)
|
||||||
|
|
||||||
|
# Section 4: Sound Notification
|
||||||
|
grp_sound = QGroupBox("Audio Notification")
|
||||||
|
sound_layout = QVBoxLayout(grp_sound)
|
||||||
|
sound_layout.setContentsMargins(15, 20, 15, 15)
|
||||||
|
sound_layout.setSpacing(10)
|
||||||
|
|
||||||
|
self.chk_sound = QCheckBox("Play sound on successful upload")
|
||||||
|
sound_layout.addWidget(self.chk_sound)
|
||||||
|
|
||||||
|
file_picker_layout = QHBoxLayout()
|
||||||
|
self.txt_sound_path = QLineEdit()
|
||||||
|
self.txt_sound_path.setPlaceholderText("Select native audio file (.wav, .ogg, .flac)...")
|
||||||
|
file_picker_layout.addWidget(self.txt_sound_path)
|
||||||
|
|
||||||
|
self.btn_browse_sound = QPushButton("Browse...")
|
||||||
|
self.btn_browse_sound.clicked.connect(self.on_browse_sound)
|
||||||
|
file_picker_layout.addWidget(self.btn_browse_sound)
|
||||||
|
|
||||||
|
self.btn_test_sound = QPushButton("Test Sound")
|
||||||
|
self.btn_test_sound.clicked.connect(self.on_test_sound)
|
||||||
|
file_picker_layout.addWidget(self.btn_test_sound)
|
||||||
|
|
||||||
|
sound_layout.addLayout(file_picker_layout)
|
||||||
|
|
||||||
|
vol_layout = QHBoxLayout()
|
||||||
|
vol_label = QLabel("Volume:")
|
||||||
|
self.slider_volume = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.slider_volume.setRange(0, 100)
|
||||||
|
self.slider_volume.setValue(15)
|
||||||
|
self.lbl_volume = QLabel("15%")
|
||||||
|
self.lbl_volume.setFixedWidth(40)
|
||||||
|
self.slider_volume.valueChanged.connect(lambda v: self.lbl_volume.setText(f"{v}%"))
|
||||||
|
vol_layout.addWidget(vol_label)
|
||||||
|
vol_layout.addWidget(self.slider_volume)
|
||||||
|
vol_layout.addWidget(self.lbl_volume)
|
||||||
|
sound_layout.addLayout(vol_layout)
|
||||||
|
|
||||||
|
main_layout.addWidget(grp_sound)
|
||||||
|
|
||||||
# Save / Cancel Buttons
|
# Save / Cancel Buttons
|
||||||
btn_layout = QHBoxLayout()
|
btn_layout = QHBoxLayout()
|
||||||
btn_layout.addStretch()
|
btn_layout.addStretch()
|
||||||
@@ -378,6 +418,18 @@ class SettingsDialog(QDialog):
|
|||||||
icon_theme = config.get("icon_theme", "dark").lower()
|
icon_theme = config.get("icon_theme", "dark").lower()
|
||||||
self.cb_icon_theme.setCurrentIndex(1 if icon_theme == "light" else 0)
|
self.cb_icon_theme.setCurrentIndex(1 if icon_theme == "light" else 0)
|
||||||
|
|
||||||
|
self.chk_sound.setChecked(config.get("play_success_sound", False))
|
||||||
|
self.txt_sound_path.setText(config.get("success_audio_path", ""))
|
||||||
|
|
||||||
|
vol = config.get("success_audio_volume", 15)
|
||||||
|
try:
|
||||||
|
vol = int(vol)
|
||||||
|
except Exception:
|
||||||
|
vol = 15
|
||||||
|
vol = min(100, max(0, vol))
|
||||||
|
self.slider_volume.setValue(vol)
|
||||||
|
self.lbl_volume.setText(f"{vol}%")
|
||||||
|
|
||||||
def on_test_connection(self):
|
def on_test_connection(self):
|
||||||
self.btn_test.setEnabled(False)
|
self.btn_test.setEnabled(False)
|
||||||
self.btn_test.setText("Testing...")
|
self.btn_test.setText("Testing...")
|
||||||
@@ -406,6 +458,32 @@ class SettingsDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
QMessageBox.warning(self, "Connection Test Failed", message)
|
QMessageBox.warning(self, "Connection Test Failed", message)
|
||||||
|
|
||||||
|
def on_browse_sound(self):
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
"Select Native Audio File for Success Notification",
|
||||||
|
"",
|
||||||
|
"Supported Audio Files (*.wav *.ogg *.oga *.flac *.aiff *.aif *.au);;WAV Audio (*.wav);;OGG Audio (*.ogg *.oga);;FLAC Audio (*.flac)"
|
||||||
|
)
|
||||||
|
if file_path:
|
||||||
|
self.txt_sound_path.setText(file_path)
|
||||||
|
|
||||||
|
def on_test_sound(self):
|
||||||
|
sound_path = self.txt_sound_path.text().strip()
|
||||||
|
if not sound_path or not os.path.exists(sound_path):
|
||||||
|
QMessageBox.warning(self, "Invalid File", "Please select a valid audio file first.")
|
||||||
|
return
|
||||||
|
ext = os.path.splitext(sound_path)[1].lower()
|
||||||
|
supported = [".wav", ".ogg", ".oga", ".flac", ".aiff", ".aif", ".au"]
|
||||||
|
if ext not in supported:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Unsupported Format",
|
||||||
|
f"The format '{ext}' is not supported natively by PipeWire/PulseAudio.\nPlease select a .wav, .ogg, or .flac file."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
play_success_sound(sound_path, volume=self.slider_volume.value())
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
config = {
|
config = {
|
||||||
"api_url": self.txt_url.text().strip(),
|
"api_url": self.txt_url.text().strip(),
|
||||||
@@ -417,7 +495,10 @@ class SettingsDialog(QDialog):
|
|||||||
"enable_notifications": self.chk_notifications.isChecked(),
|
"enable_notifications": self.chk_notifications.isChecked(),
|
||||||
"show_progress_dialog": self.chk_progress.isChecked(),
|
"show_progress_dialog": self.chk_progress.isChecked(),
|
||||||
"autostart": self.chk_autostart.isChecked(),
|
"autostart": self.chk_autostart.isChecked(),
|
||||||
"icon_theme": "light" if self.cb_icon_theme.currentIndex() == 1 else "dark"
|
"icon_theme": "light" if self.cb_icon_theme.currentIndex() == 1 else "dark",
|
||||||
|
"play_success_sound": self.chk_sound.isChecked(),
|
||||||
|
"success_audio_path": self.txt_sound_path.text().strip(),
|
||||||
|
"success_audio_volume": self.slider_volume.value()
|
||||||
}
|
}
|
||||||
|
|
||||||
if save_config(config):
|
if save_config(config):
|
||||||
@@ -519,6 +600,190 @@ def generate_thumbnail(file_path: str) -> str:
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def format_size(bytes_val: int) -> str:
|
||||||
|
if bytes_val < 1024:
|
||||||
|
return f"{bytes_val} B"
|
||||||
|
elif bytes_val < 1024 * 1024:
|
||||||
|
return f"{bytes_val / 1024:.1f} KB"
|
||||||
|
elif bytes_val < 1024 * 1024 * 1024:
|
||||||
|
return f"{bytes_val / (1024 * 1024):.1f} MB"
|
||||||
|
else:
|
||||||
|
return f"{bytes_val / (1024 * 1024 * 1024):.2f} GB"
|
||||||
|
|
||||||
|
def format_speed(bytes_per_sec: float) -> str:
|
||||||
|
if bytes_per_sec < 1024:
|
||||||
|
return f"{int(bytes_per_sec)} B/s"
|
||||||
|
elif bytes_per_sec < 1024 * 1024:
|
||||||
|
return f"{bytes_per_sec / 1024:.1f} KB/s"
|
||||||
|
else:
|
||||||
|
return f"{bytes_per_sec / (1024 * 1024):.1f} MB/s"
|
||||||
|
|
||||||
|
def play_success_sound(audio_path=None, volume=None):
|
||||||
|
config = get_env_config()
|
||||||
|
if not audio_path:
|
||||||
|
audio_path = config.get("success_audio_path", "")
|
||||||
|
if not audio_path or not os.path.exists(audio_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
if volume is None:
|
||||||
|
volume = config.get("success_audio_volume", 15)
|
||||||
|
|
||||||
|
try:
|
||||||
|
volume = min(100, max(0, int(volume)))
|
||||||
|
except Exception:
|
||||||
|
volume = 15
|
||||||
|
|
||||||
|
ext = os.path.splitext(audio_path)[1].lower()
|
||||||
|
supported = [".wav", ".ogg", ".oga", ".flac", ".aiff", ".aif", ".au"]
|
||||||
|
if ext not in supported:
|
||||||
|
return
|
||||||
|
|
||||||
|
vol_float = volume / 100.0
|
||||||
|
pa_vol_int = int(65536 * vol_float)
|
||||||
|
|
||||||
|
# Use native Linux audio daemons (pw-play, paplay, canberra-gtk-play, aplay)
|
||||||
|
for player in ["pw-play", "paplay", "canberra-gtk-play", "aplay"]:
|
||||||
|
if shutil.which(player):
|
||||||
|
try:
|
||||||
|
if player == "pw-play":
|
||||||
|
cmd = [player, "--volume", f"{vol_float:.2f}", audio_path]
|
||||||
|
elif player == "paplay":
|
||||||
|
cmd = [player, f"--volume={pa_vol_int}", audio_path]
|
||||||
|
elif player == "canberra-gtk-play":
|
||||||
|
cmd = [player, "-f", audio_path]
|
||||||
|
else:
|
||||||
|
cmd = [player, audio_path]
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to Qt QSoundEffect for WAV files
|
||||||
|
if ext == ".wav":
|
||||||
|
try:
|
||||||
|
from PySide6.QtMultimedia import QSoundEffect
|
||||||
|
effect = QSoundEffect()
|
||||||
|
effect.setSource(QUrl.fromLocalFile(audio_path))
|
||||||
|
effect.setVolume(vol_float)
|
||||||
|
effect.play()
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app:
|
||||||
|
if not hasattr(app, "_sound_effects"):
|
||||||
|
app._sound_effects = []
|
||||||
|
app._sound_effects.append(effect)
|
||||||
|
effect.playingChanged.connect(
|
||||||
|
lambda: app._sound_effects.remove(effect) if not effect.isPlaying() and effect in getattr(app, "_sound_effects", []) else None
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CurlUploadThread(QThread):
|
||||||
|
progress = Signal(int, int) # bytes_sent, bytes_total
|
||||||
|
finished = Signal(bool, str) # success, response_or_error_msg
|
||||||
|
|
||||||
|
def __init__(self, api_url, api_key, file_path, rating, tags, visibility, is_oc, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.api_url = api_url
|
||||||
|
self.api_key = api_key
|
||||||
|
self.file_path = file_path
|
||||||
|
self.rating = rating
|
||||||
|
self.tags = tags
|
||||||
|
self.visibility = visibility
|
||||||
|
self.is_oc = is_oc
|
||||||
|
self.process = None
|
||||||
|
self.file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||||
|
self.aborted = False
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self.aborted = True
|
||||||
|
if self.process:
|
||||||
|
try:
|
||||||
|
self.process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
mime_type, _ = mimetypes.guess_type(self.file_path)
|
||||||
|
if not mime_type:
|
||||||
|
ext = os.path.splitext(self.file_path)[1].lower()
|
||||||
|
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']:
|
||||||
|
mime_type = f"image/{ext.lstrip('.')}"
|
||||||
|
if mime_type == "image/jpg":
|
||||||
|
mime_type = "image/jpeg"
|
||||||
|
elif ext in ['.mp4', '.webm', '.mkv', '.mov', '.avi']:
|
||||||
|
mime_type = f"video/{ext.lstrip('.')}"
|
||||||
|
else:
|
||||||
|
mime_type = "image/png"
|
||||||
|
|
||||||
|
file_param = f"file=@{self.file_path}"
|
||||||
|
if mime_type:
|
||||||
|
file_param += f";type={mime_type}"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"curl", "-#",
|
||||||
|
"-F", file_param
|
||||||
|
]
|
||||||
|
if self.api_key:
|
||||||
|
cmd.extend(["-H", f"X-Api-Key: {self.api_key}"])
|
||||||
|
if self.rating:
|
||||||
|
cmd.extend(["-F", f"rating={self.rating}"])
|
||||||
|
if self.tags:
|
||||||
|
cmd.extend(["-F", f"tags={self.tags}"])
|
||||||
|
if self.visibility != "":
|
||||||
|
cmd.extend(["-F", f"visibility={self.visibility}"])
|
||||||
|
if self.is_oc != "0":
|
||||||
|
cmd.extend(["-F", f"is_oc={self.is_oc}"])
|
||||||
|
|
||||||
|
cmd.append(self.api_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
buffer = ""
|
||||||
|
while True:
|
||||||
|
char = self.process.stderr.read(1)
|
||||||
|
if not char and self.process.poll() is not None:
|
||||||
|
break
|
||||||
|
if char in ("\r", "\n"):
|
||||||
|
matches = re.findall(r'(\d+(?:\.\d+)?)\s*%', buffer)
|
||||||
|
if matches:
|
||||||
|
try:
|
||||||
|
pct = float(matches[-1])
|
||||||
|
bytes_sent = int(self.file_size * (pct / 100.0))
|
||||||
|
self.progress.emit(bytes_sent, self.file_size)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
buffer = ""
|
||||||
|
else:
|
||||||
|
buffer += char
|
||||||
|
|
||||||
|
stdout_data, stderr_data = self.process.communicate()
|
||||||
|
exit_code = self.process.returncode
|
||||||
|
|
||||||
|
if self.aborted:
|
||||||
|
self.finished.emit(False, "ABORTED")
|
||||||
|
elif exit_code == 0:
|
||||||
|
self.finished.emit(True, stdout_data)
|
||||||
|
else:
|
||||||
|
self.finished.emit(False, stderr_data or f"curl exited with code {exit_code}")
|
||||||
|
except Exception as e:
|
||||||
|
if self.aborted:
|
||||||
|
self.finished.emit(False, "ABORTED")
|
||||||
|
else:
|
||||||
|
self.finished.emit(False, str(e))
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# System Tray Application Manager
|
# System Tray Application Manager
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -554,6 +819,11 @@ class SystemTrayApp(QObject):
|
|||||||
clipboard_action.triggered.connect(self.on_upload_clipboard)
|
clipboard_action.triggered.connect(self.on_upload_clipboard)
|
||||||
self.menu.addAction(clipboard_action)
|
self.menu.addAction(clipboard_action)
|
||||||
|
|
||||||
|
self.abort_action = QAction("Abort Upload", self.menu)
|
||||||
|
self.abort_action.setEnabled(False)
|
||||||
|
self.abort_action.triggered.connect(self.on_abort_upload)
|
||||||
|
self.menu.addAction(self.abort_action)
|
||||||
|
|
||||||
settings_action = QAction("Settings...", self.menu)
|
settings_action = QAction("Settings...", self.menu)
|
||||||
settings_action.triggered.connect(self.on_open_settings)
|
settings_action.triggered.connect(self.on_open_settings)
|
||||||
self.menu.addAction(settings_action)
|
self.menu.addAction(settings_action)
|
||||||
@@ -641,66 +911,67 @@ class SystemTrayApp(QObject):
|
|||||||
visibility = str(config.get("default_visibility", "0"))
|
visibility = str(config.get("default_visibility", "0"))
|
||||||
is_oc = "1" if config.get("default_is_oc", False) else "0"
|
is_oc = "1" if config.get("default_is_oc", False) else "0"
|
||||||
|
|
||||||
self.tray.setIcon(create_digit_icon(0))
|
|
||||||
self.tray.setToolTip("Uploading: 0%")
|
|
||||||
|
|
||||||
request = QNetworkRequest(QUrl(api_url))
|
|
||||||
if api_key:
|
|
||||||
request.setRawHeader(b"X-Api-Key", api_key.encode("utf-8"))
|
|
||||||
|
|
||||||
multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
|
||||||
|
|
||||||
file_part = QHttpPart()
|
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
file_part.setHeader(QNetworkRequest.ContentDispositionHeader, f'form-data; name="file"; filename="{filename}"')
|
self.upload_filename = filename
|
||||||
|
self.upload_start_time = time.time()
|
||||||
|
|
||||||
mime_type, _ = mimetypes.guess_type(file_path)
|
self.tray.setIcon(create_digit_icon(0))
|
||||||
if not mime_type:
|
self.tray.setToolTip(f"Uploading {filename}...\n0% (0 B)")
|
||||||
mime_type = "image/png" if file_path.lower().endswith(".png") else "application/octet-stream"
|
|
||||||
file_part.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
|
|
||||||
|
|
||||||
qfile = QFile(file_path)
|
self.upload_thread = CurlUploadThread(api_url, api_key, file_path, rating, tags, visibility, is_oc)
|
||||||
if not qfile.open(QIODevice.ReadOnly):
|
self.upload_thread.progress.connect(self.on_upload_progress)
|
||||||
self.tray.setIcon(self.default_icon)
|
self.upload_thread.finished.connect(lambda ok, resp: self.on_upload_finished(ok, resp, file_path))
|
||||||
self.show_message("Upload Error", f"Cannot open file: {file_path}", QSystemTrayIcon.Critical)
|
self.upload_thread.start()
|
||||||
return
|
|
||||||
|
|
||||||
file_part.setBodyDevice(qfile)
|
self.abort_action.setEnabled(True)
|
||||||
qfile.setParent(multi_part)
|
|
||||||
multi_part.append(file_part)
|
|
||||||
|
|
||||||
for name, val in [("rating", rating), ("tags", tags), ("visibility", visibility), ("is_oc", is_oc)]:
|
|
||||||
if val != "":
|
|
||||||
p = QHttpPart()
|
|
||||||
p.setHeader(QNetworkRequest.ContentDispositionHeader, f'form-data; name="{name}"')
|
|
||||||
p.setBody(str(val).encode("utf-8"))
|
|
||||||
multi_part.append(p)
|
|
||||||
|
|
||||||
reply = self.network_manager.post(request, multi_part)
|
|
||||||
multi_part.setParent(reply)
|
|
||||||
reply.qfile = qfile
|
|
||||||
reply.multi_part = multi_part
|
|
||||||
self.current_reply = reply
|
|
||||||
|
|
||||||
reply.uploadProgress.connect(self.on_upload_progress)
|
|
||||||
reply.finished.connect(lambda: self.on_upload_finished(reply, file_path))
|
|
||||||
|
|
||||||
def on_upload_progress(self, bytes_sent, bytes_total):
|
def on_upload_progress(self, bytes_sent, bytes_total):
|
||||||
if bytes_total > 0:
|
if bytes_total > 0:
|
||||||
pct = min(100, max(0, int((bytes_sent / bytes_total) * 100)))
|
pct = min(100, max(0, int((bytes_sent / bytes_total) * 100)))
|
||||||
self.tray.setIcon(create_digit_icon(pct))
|
self.tray.setIcon(create_digit_icon(pct))
|
||||||
self.tray.setToolTip(f"Uploading: {pct}%")
|
|
||||||
|
|
||||||
def on_upload_finished(self, reply, file_path):
|
elapsed = time.time() - getattr(self, "upload_start_time", time.time())
|
||||||
|
speed = (bytes_sent / elapsed) if elapsed > 0.1 else 0
|
||||||
|
|
||||||
|
sent_str = format_size(bytes_sent)
|
||||||
|
total_str = format_size(bytes_total)
|
||||||
|
filename = getattr(self, "upload_filename", "file")
|
||||||
|
|
||||||
|
if pct >= 99:
|
||||||
|
tooltip = f"Uploading: {filename}\n99% ({total_str})\nProcessing upload on server..."
|
||||||
|
else:
|
||||||
|
speed_str = format_speed(speed)
|
||||||
|
remaining_bytes = bytes_total - bytes_sent
|
||||||
|
eta_sec = int(remaining_bytes / speed) if speed > 0 else 0
|
||||||
|
if eta_sec < 60:
|
||||||
|
eta_str = f"{eta_sec}s"
|
||||||
|
elif eta_sec < 3600:
|
||||||
|
eta_str = f"{eta_sec // 60}m {eta_sec % 60}s"
|
||||||
|
else:
|
||||||
|
eta_str = f"{eta_sec // 3600}h {(eta_sec % 3600) // 60}m"
|
||||||
|
|
||||||
|
tooltip = f"Uploading: {filename}\n{pct}% ({sent_str} / {total_str})\nSpeed: {speed_str} | ETA: {eta_str}"
|
||||||
|
|
||||||
|
self.tray.setToolTip(tooltip)
|
||||||
|
|
||||||
|
def on_abort_upload(self):
|
||||||
|
if hasattr(self, "upload_thread") and self.upload_thread:
|
||||||
|
self.upload_thread.abort()
|
||||||
|
|
||||||
|
def on_upload_finished(self, success, response_text, file_path):
|
||||||
|
self.abort_action.setEnabled(False)
|
||||||
self.tray.setIcon(self.default_icon)
|
self.tray.setIcon(self.default_icon)
|
||||||
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
|
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
|
||||||
|
|
||||||
try:
|
if not success:
|
||||||
err_body = reply.readAll().data().decode("utf-8", errors="ignore")
|
if response_text == "ABORTED":
|
||||||
|
self.show_message("Upload Aborted", "The active upload was canceled.", QSystemTrayIcon.Information)
|
||||||
|
else:
|
||||||
|
self.show_message("Upload Failed", response_text, QSystemTrayIcon.Critical)
|
||||||
|
return
|
||||||
|
|
||||||
if reply.error() == QNetworkReply.NoError:
|
|
||||||
try:
|
try:
|
||||||
res = json.loads(err_body)
|
res = json.loads(response_text)
|
||||||
if res.get("success") and res.get("url"):
|
if res.get("success") and res.get("url"):
|
||||||
item_url = res.get("url")
|
item_url = res.get("url")
|
||||||
QApplication.clipboard().setText(item_url)
|
QApplication.clipboard().setText(item_url)
|
||||||
@@ -709,18 +980,16 @@ class SystemTrayApp(QObject):
|
|||||||
msg = res.get("msg", "Upload failed")
|
msg = res.get("msg", "Upload failed")
|
||||||
self.show_message("Upload Failed", msg, QSystemTrayIcon.Critical)
|
self.show_message("Upload Failed", msg, QSystemTrayIcon.Critical)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.show_message("Upload Error", f"Error parsing response: {e}", QSystemTrayIcon.Critical)
|
self.show_message("Upload Error", f"Error parsing response: {e}\nResponse: {response_text}", QSystemTrayIcon.Critical)
|
||||||
else:
|
|
||||||
server_msg = err_body if err_body else reply.errorString()
|
|
||||||
self.show_message("Upload Failed", f"Server Response: {server_msg}", QSystemTrayIcon.Critical)
|
|
||||||
except Exception as e:
|
|
||||||
self.show_message("Upload Error", f"Unexpected error: {e}", QSystemTrayIcon.Critical)
|
|
||||||
finally:
|
|
||||||
reply.deleteLater()
|
|
||||||
self.current_reply = None
|
|
||||||
|
|
||||||
def show_success_notification(self, file_path, item_url):
|
def show_success_notification(self, file_path, item_url):
|
||||||
config = get_env_config()
|
config = get_env_config()
|
||||||
|
if config.get("play_success_sound", False) or config.get("success_audio_path"):
|
||||||
|
audio_path = config.get("success_audio_path", "")
|
||||||
|
volume = config.get("success_audio_volume", 15)
|
||||||
|
if audio_path:
|
||||||
|
play_success_sound(audio_path, volume=volume)
|
||||||
|
|
||||||
if not config.get("enable_notifications", True):
|
if not config.get("enable_notifications", True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user