fix desktop app and android app

This commit is contained in:
2026-08-13 19:22:04 +02:00
parent 9050faf0a4
commit 971f9d647e
8 changed files with 391 additions and 162 deletions

View File

@@ -10,7 +10,8 @@ data class Instance(
var apiKey: String,
var defaultRating: String = "none",
var defaultTags: String = "",
var defaultVisibility: String = "0"
var defaultVisibility: String = "0",
var resultLinkType: String = "post"
) {
fun toJsonObject(): JSONObject {
return JSONObject().apply {
@@ -21,6 +22,7 @@ data class Instance(
put("defaultRating", defaultRating)
put("defaultTags", defaultTags)
put("defaultVisibility", defaultVisibility)
put("resultLinkType", resultLinkType)
}
}
@@ -35,7 +37,8 @@ data class Instance(
apiKey = json.optString("apiKey", ""),
defaultRating = json.optString("defaultRating", "none"),
defaultTags = json.optString("defaultTags", ""),
defaultVisibility = json.optString("defaultVisibility", "0")
defaultVisibility = json.optString("defaultVisibility", "0"),
resultLinkType = json.optString("resultLinkType", "post")
)
}
}

View File

@@ -31,6 +31,7 @@ class MainActivity : AppCompatActivity() {
setupRatingSpinner()
setupVisibilitySpinner()
setupLinkTypeSpinner()
setupInstanceSpinner()
binding.btnSave.setOnClickListener {
@@ -63,6 +64,12 @@ class MainActivity : AppCompatActivity() {
binding.spinnerVisibility.adapter = adapter
}
private fun setupLinkTypeSpinner() {
val types = arrayOf("Post Link (Viewer)", "Direct File Link")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, types)
binding.spinnerLinkType.adapter = adapter
}
private fun setupInstanceSpinner() {
currentInstances = prefs.getInstances()
if (currentInstances.isEmpty()) {
@@ -106,6 +113,12 @@ class MainActivity : AppCompatActivity() {
"2" -> binding.spinnerVisibility.setSelection(2)
else -> binding.spinnerVisibility.setSelection(0)
}
if (instance.resultLinkType == "direct") {
binding.spinnerLinkType.setSelection(1)
} else {
binding.spinnerLinkType.setSelection(0)
}
}
private fun saveSettings() {
@@ -125,6 +138,8 @@ class MainActivity : AppCompatActivity() {
currentInstance.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString()
currentInstance.resultLinkType = if (binding.spinnerLinkType.selectedItemPosition == 1) "direct" else "post"
prefs.updateInstance(currentInstance)
// Refresh spinner to show updated name

View File

@@ -6,11 +6,16 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.fuggloader.app.databinding.ActivityShareBinding
import com.fuggloader.app.databinding.ItemUploadResultBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -21,7 +26,10 @@ class ShareActivity : AppCompatActivity() {
private lateinit var prefs: PreferencesManager
private lateinit var uploader: UploaderService
private var uploadedUrl: String? = null
private val uploadedResults = mutableListOf<UploadResult>()
private lateinit var resultAdapter: UploadResultAdapter
data class UploadResult(val name: String, val url: String)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -31,6 +39,8 @@ class ShareActivity : AppCompatActivity() {
prefs = PreferencesManager(this)
uploader = UploaderService(prefs)
setupRecyclerView()
if (!prefs.isConfigured()) {
Toast.makeText(this, "fuggloader is not configured. Please open app settings first.", Toast.LENGTH_LONG).show()
val intent = Intent(this, MainActivity::class.java)
@@ -40,17 +50,10 @@ class ShareActivity : AppCompatActivity() {
}
binding.btnCopy.setOnClickListener {
uploadedUrl?.let { url ->
copyToClipboard(url)
Toast.makeText(this, "URL copied to clipboard!", Toast.LENGTH_SHORT).show()
}
}
binding.btnOpen.setOnClickListener {
uploadedUrl?.let { url ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(browserIntent)
finish()
if (uploadedResults.isNotEmpty()) {
val text = uploadedResults.joinToString("\n") { it.url }
copyToClipboard(text)
Toast.makeText(this, "All URLs copied to clipboard!", Toast.LENGTH_SHORT).show()
}
}
@@ -61,65 +64,214 @@ class ShareActivity : AppCompatActivity() {
prepareShare(intent)
}
private fun prepareShare(intent: Intent) {
val instances = prefs.getInstances()
if (instances.size > 1) {
val names = instances.map { it.name }.toTypedArray()
AlertDialog.Builder(this)
.setTitle("Select Instance")
.setItems(names) { _, which ->
handleShareIntent(instances[which], intent)
private fun setupRecyclerView() {
resultAdapter = UploadResultAdapter(uploadedResults) { result, action ->
when (action) {
ResultAction.COPY -> {
copyToClipboard(result.url)
Toast.makeText(this, "URL copied!", Toast.LENGTH_SHORT).show()
}
ResultAction.OPEN -> {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(result.url))
startActivity(browserIntent)
}
.setNegativeButton("Cancel") { _, _ -> finish() }
.setCancelable(false)
.show()
} else {
val target = instances.firstOrNull()
if (target != null) {
handleShareIntent(target, intent)
} else {
showError("No instance configured")
}
}
binding.rvResults.layoutManager = LinearLayoutManager(this)
binding.rvResults.adapter = resultAdapter
}
enum class ResultAction { COPY, OPEN }
inner class UploadResultAdapter(
private val items: List<UploadResult>,
private val onAction: (UploadResult, ResultAction) -> Unit
) : RecyclerView.Adapter<UploadResultAdapter.ViewHolder>() {
inner class ViewHolder(val itemBinding: ItemUploadResultBinding) : RecyclerView.ViewHolder(itemBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val b = ItemUploadResultBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(b)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemBinding.tvFileName.text = item.name
holder.itemBinding.tvFileUrl.text = item.url
holder.itemBinding.btnItemCopy.setOnClickListener { onAction(item, ResultAction.COPY) }
holder.itemBinding.btnItemOpen.setOnClickListener { onAction(item, ResultAction.OPEN) }
}
override fun getItemCount() = items.size
}
private fun prepareShare(intent: Intent) {
val target = prefs.getActiveInstance()
if (target != null) {
handleShareIntent(target, intent)
} else {
showError("No instance configured")
}
}
private fun handleShareIntent(instance: Instance, intent: Intent) {
val action = intent.action
val type = intent.type
val uris = mutableListOf<Uri>()
var sharedUrl: String? = null
if (Intent.ACTION_SEND == action && type != null) {
if ("text/plain" == type) {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (!sharedText.isNull_or_blank_url()) {
val targetUrl = extractUrl(sharedText!!)
processUrlUpload(instance, targetUrl)
sharedUrl = extractUrl(sharedText!!)
} else {
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (streamUri != null) {
processFileUpload(instance, streamUri)
} else {
showError("No valid file or URL found in share intent")
uris.add(streamUri)
}
}
} else {
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (streamUri != null) {
processFileUpload(instance, streamUri)
} else {
showError("No stream URI found for file share")
uris.add(streamUri)
}
}
} else if (Intent.ACTION_SEND_MULTIPLE == action) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (!uris.isNullOrEmpty()) {
// Upload first file in batch for now
processFileUpload(instance, uris[0])
} else {
showError("No files found in multi-share intent")
val streamUris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (streamUris != null) {
uris.addAll(streamUris)
}
} else {
showError("Unsupported share action: $action")
}
if (sharedUrl != null) {
processUrlUpload(instance, sharedUrl)
} else if (uris.isNotEmpty()) {
processBatchUpload(instance, uris)
} else {
showError("No valid file or URL found in share intent")
}
}
private fun processBatchUpload(instance: Instance, uris: List<Uri>) {
binding.progressBar.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0
CoroutineScope(Dispatchers.Main).launch {
for ((index, uri) in uris.withIndex()) {
val displayIndex = index + 1
val fileName = getFileName(uri) ?: "File $displayIndex"
binding.tvStatus.text = "Uploading $displayIndex of ${uris.size}: $fileName"
binding.tvPercent.text = "0%"
binding.progressBar.progress = 0
val result = uploader.uploadFile(
contentResolver = contentResolver,
uri = uri,
instance = instance,
onProgress = { pct ->
runOnUiThread {
binding.progressBar.progress = pct
binding.tvPercent.text = "$pct%"
}
}
)
result.fold(
onSuccess = { url ->
uploadedResults.add(UploadResult(fileName, url))
runOnUiThread {
resultAdapter.notifyItemInserted(uploadedResults.size - 1)
binding.rvResults.visibility = View.VISIBLE
}
if (index == uris.size - 1) {
showSuccess()
}
},
onFailure = { err ->
showError("Upload $displayIndex failed: ${err.message}")
return@launch // Stop on first error
}
)
}
}
}
private fun processUrlUpload(instance: Instance, url: String) {
binding.tvStatus.text = "Uploading URL to ${instance.name}..."
binding.progressBar.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = true
binding.tvPercent.text = ""
CoroutineScope(Dispatchers.Main).launch {
val result = uploader.uploadUrl(url, instance)
result.fold(
onSuccess = { postUrl ->
uploadedResults.add(UploadResult("Shared URL", postUrl))
runOnUiThread {
resultAdapter.notifyItemInserted(uploadedResults.size - 1)
binding.rvResults.visibility = View.VISIBLE
}
showSuccess()
},
onFailure = { err -> showError(err.message ?: "URL Upload failed") }
)
}
}
private fun showSuccess() {
if (uploadedResults.isEmpty()) return
binding.progressBar.visibility = View.GONE
binding.tvPercent.text = "100%"
if (uploadedResults.size > 1) {
binding.tvStatus.text = "All ${uploadedResults.size} uploads complete!"
binding.btnCopy.text = "Copy All Links"
val allLinks = uploadedResults.joinToString("\n") { it.url }
copyToClipboard(allLinks)
} else {
binding.tvStatus.text = "Upload complete!"
binding.btnCopy.text = "Copy Link"
copyToClipboard(uploadedResults.first().url)
}
binding.layoutButtons.visibility = View.VISIBLE
Toast.makeText(this, "Uploaded! Links copied to clipboard.", Toast.LENGTH_LONG).show()
}
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (index != -1) result = cursor.getString(index)
}
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/') ?: -1
if (cut != -1) result = result?.substring(cut + 1)
}
return result
}
private fun showError(msg: String) {
binding.progressBar.visibility = View.GONE
binding.tvStatus.text = "Error: $msg"
binding.btnClose.visibility = View.VISIBLE
}
private fun copyToClipboard(text: String) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("fuggloader link", text)
clipboard.setPrimaryClip(clip)
}
private fun String?.isNull_or_blank_url(): Boolean {
@@ -138,71 +290,4 @@ class ShareActivity : AppCompatActivity() {
}
return trimmed
}
private fun processFileUpload(instance: Instance, uri: Uri) {
binding.tvStatus.text = "Uploading to ${instance.name}..."
binding.progressBar.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0
CoroutineScope(Dispatchers.Main).launch {
val result = uploader.uploadFile(
contentResolver = contentResolver,
uri = uri,
instance = instance,
onProgress = { pct ->
runOnUiThread {
binding.progressBar.progress = pct
binding.tvPercent.text = "$pct%"
}
}
)
result.fold(
onSuccess = { url -> showSuccess(url) },
onFailure = { err -> showError(err.message ?: "Upload failed") }
)
}
}
private fun processUrlUpload(instance: Instance, url: String) {
binding.tvStatus.text = "Uploading to ${instance.name}..."
binding.progressBar.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = true
binding.tvPercent.text = ""
CoroutineScope(Dispatchers.Main).launch {
val result = uploader.uploadUrl(url, instance)
result.fold(
onSuccess = { postUrl -> showSuccess(postUrl) },
onFailure = { err -> showError(err.message ?: "URL Upload failed") }
)
}
}
private fun showSuccess(url: String) {
uploadedUrl = url
copyToClipboard(url)
binding.progressBar.visibility = View.GONE
binding.tvPercent.text = "100%"
binding.tvStatus.text = "Upload complete! Link copied to clipboard."
binding.tvResultUrl.visibility = View.VISIBLE
binding.tvResultUrl.text = url
binding.layoutButtons.visibility = View.VISIBLE
Toast.makeText(this, "Uploaded! Link copied to clipboard.", Toast.LENGTH_LONG).show()
}
private fun showError(msg: String) {
binding.progressBar.visibility = View.GONE
binding.tvStatus.text = "Error: $msg"
binding.btnClose.visibility = View.VISIBLE
}
private fun copyToClipboard(text: String) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("fuggloader link", text)
clipboard.setPrimaryClip(clip)
}
}

View File

@@ -89,7 +89,7 @@ class UploaderService(private val prefs: PreferencesManager) {
if (response.isSuccessful) {
onProgress(100)
val postUrl = parsePostUrl(responseBody)
val postUrl = parsePostUrl(responseBody, targetInstance.resultLinkType)
Result.success(postUrl)
} else {
Log.e("UploaderService", "Upload failed: HTTP ${response.code}, Body: $responseBody")
@@ -134,7 +134,7 @@ class UploaderService(private val prefs: PreferencesManager) {
val responseBody = response.body?.string() ?: ""
if (response.isSuccessful) {
val postUrl = parsePostUrl(responseBody)
val postUrl = parsePostUrl(responseBody, targetInstance.resultLinkType)
Result.success(postUrl)
} else {
Log.e("UploaderService", "URL Upload failed: HTTP ${response.code}, Body: $responseBody")
@@ -172,23 +172,63 @@ class UploaderService(private val prefs: PreferencesManager) {
}
}
private fun parsePostUrl(jsonResponse: String): String {
private fun parsePostUrl(jsonResponse: String, linkType: String): String {
Log.d("UploaderService", "Parsing response (linkType=$linkType): $jsonResponse")
return try {
val json = JSONObject(jsonResponse)
if (json.has("url")) {
json.getString("url")
} else if (json.has("post_url")) {
json.getString("post_url")
} else if (json.has("link")) {
json.getString("link")
// For f0ckm API:
// "url" is the viewer post link (e.g. https://fuggloader.com/abcdef)
// "file_url" is the direct link (e.g. https://fuggloader.com/b/uuid.jpg)
if (linkType == "direct") {
if (json.has("file_url") && !json.isNull("file_url")) {
return json.getString("file_url")
}
} else {
jsonResponse
if (json.has("url") && !json.isNull("url")) {
return json.getString("url")
}
}
// Fallback logic for other potential APIs or if preferred key is missing
val keysToTry = if (linkType == "direct") {
listOf("file_url", "direct_url", "link", "url", "post_url")
} else {
listOf("url", "post_url", "link", "file_url")
}
findValue(json, keysToTry) ?: jsonResponse
} catch (e: Exception) {
Log.e("UploaderService", "Error parsing JSON", e)
jsonResponse
}
}
private fun findValue(json: JSONObject, keys: List<String>): String? {
// First try root level
for (key in keys) {
if (json.has(key) && !json.isNull(key)) {
val value = json.optString(key)
if (value.isNotBlank()) return value
}
}
// Then try common nested objects like "data" or "image"
val nestedObjects = listOf("data", "image", "success") // some use "success" as a wrapper
for (nested in nestedObjects) {
if (json.has(nested) && !json.isNull(nested)) {
val obj = json.optJSONObject(nested)
if (obj != null) {
val found = findValue(obj, keys)
if (found != null) return found
}
}
}
return null
}
private fun getFileName(contentResolver: ContentResolver, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {

View File

@@ -190,6 +190,23 @@
android:background="#1E1E24"
android:padding="8dp" />
<!-- Result Link Type -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Result Link Type"
android:textColor="#E0E0E0"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerLinkType"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="6dp"
android:background="#1E1E24"
android:padding="8dp" />
<!-- Buttons -->
<LinearLayout
android:layout_width="match_parent"

View File

@@ -48,15 +48,12 @@
android:textColor="#808090"
android:textSize="12sp" />
<TextView
android:id="@+id/tvResultUrl"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvResults"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="#121216"
android:padding="8dp"
android:textColor="#A5D6A7"
android:textSize="13sp"
android:maxHeight="300dp"
android:visibility="gone" />
<LinearLayout
@@ -69,21 +66,11 @@
<Button
android:id="@+id/btnCopy"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:backgroundTint="#2E2E3A"
android:text="Copy Link" />
<Button
android:id="@+id/btnOpen"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:backgroundTint="#6200EE"
android:text="Open Post" />
</LinearLayout>
<Button

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="8dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvFileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:textStyle="bold"
android:ellipsize="middle"
android:singleLine="true" />
<TextView
android:id="@+id/tvFileUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#A5D6A7"
android:textSize="12sp"
android:ellipsize="end"
android:singleLine="true" />
</LinearLayout>
<Button
android:id="@+id/btnItemCopy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:paddingHorizontal="8dp"
android:text="Copy"
android:textColor="#B0B0C0"
android:textSize="12sp" />
<Button
android:id="@+id/btnItemOpen"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minWidth="0dp"
android:paddingHorizontal="8dp"
android:text="Open"
android:textColor="#B0B0C0"
android:textSize="12sp" />
</LinearLayout>

64
gui.py
View File

@@ -18,7 +18,7 @@ import signal
from http.server import HTTPServer, BaseHTTPRequestHandler
from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess
from PySide6.QtGui import QIcon, QAction, QPixmap, QPainter, QColor, QFont, QPen, QImage, QDesktopServices, QDrag, QClipboard
from PySide6.QtGui import QIcon, QAction, QActionGroup, QPixmap, QPainter, QColor, QFont, QPen, QImage, QDesktopServices, QDrag, QClipboard
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket
from PySide6.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
@@ -688,21 +688,29 @@ class SettingsDialog(QDialog):
self.btn_toggle_key.setText("Show")
def refresh_instance_combo(self):
prev_ignore = self._ignore_instance_signals
self._ignore_instance_signals = True
self.cb_instance.clear()
for inst in self.instances:
self.cb_instance.addItem(inst.get("name", "Unnamed Instance"))
try:
self.cb_instance.clear()
for inst in self.instances:
self.cb_instance.addItem(inst.get("name", "Unnamed Instance"))
if 0 <= self.current_instance_index < len(self.instances):
self.cb_instance.setCurrentIndex(self.current_instance_index)
self._ignore_instance_signals = False
self.load_instance_fields(self.current_instance_index)
if 0 <= self.current_instance_index < len(self.instances):
self.cb_instance.setCurrentIndex(self.current_instance_index)
self.load_instance_fields(self.current_instance_index)
finally:
self._ignore_instance_signals = prev_ignore
def load_instance_fields(self, index):
if 0 <= index < len(self.instances):
inst = self.instances[index]
self.txt_url.setText(inst.get("api_url", ""))
self.txt_key.setText(inst.get("api_key", ""))
prev_ignore = self._ignore_instance_signals
self._ignore_instance_signals = True
try:
inst = self.instances[index]
self.txt_url.setText(inst.get("api_url", ""))
self.txt_key.setText(inst.get("api_key", ""))
finally:
self._ignore_instance_signals = prev_ignore
def on_instance_changed(self, new_index):
if self._ignore_instance_signals or new_index < 0 or new_index >= len(self.instances):
@@ -1715,11 +1723,11 @@ class SystemTrayApp(QObject):
self.menu.addAction(self.abort_action)
settings_action = QAction("Settings...", self.menu)
settings_action.triggered.connect(self.on_open_settings)
settings_action.triggered.connect(lambda checked=False: self.on_open_settings())
self.menu.addAction(settings_action)
gallery_action = QAction("Recent Uploads Gallery", self.menu)
gallery_action.triggered.connect(self.on_open_gallery)
gallery_action.triggered.connect(lambda checked=False: self.on_open_gallery())
self.menu.addAction(gallery_action)
self.menu.addSeparator()
@@ -1747,6 +1755,8 @@ class SystemTrayApp(QObject):
def rebuild_instance_menu(self):
self.instance_menu.clear()
self.instance_actions = []
config = get_env_config()
instances = config.get("instances", [])
active_idx = config.get("active_instance_index", 0)
@@ -1754,22 +1764,27 @@ class SystemTrayApp(QObject):
active_name = config.get("active_instance_name", "Default Instance")
self.instance_menu.setTitle(f"Instance: {active_name}")
self.instance_action_group = QActionGroup(self.instance_menu)
self.instance_action_group.setExclusive(True)
for idx, inst in enumerate(instances):
name = inst.get("name", f"Instance {idx + 1}")
url = inst.get("api_url", "")
action = QAction(name, self.instance_menu)
action.setCheckable(True)
self.instance_action_group.addAction(action)
if url:
action.setToolTip(url)
action.setCheckable(True)
if idx == active_idx:
action.setChecked(True)
action.triggered.connect(lambda checked=False, i=idx: self.switch_active_instance(i))
self.instance_menu.addAction(action)
self.instance_actions.append(action)
self.instance_menu.addSeparator()
manage_action = QAction("Manage Instances...", self.instance_menu)
manage_action.triggered.connect(self.on_open_settings)
manage_action.triggered.connect(lambda checked=False: self.on_open_settings())
self.instance_menu.addAction(manage_action)
def switch_active_instance(self, index):
@@ -1785,8 +1800,12 @@ class SystemTrayApp(QObject):
name = active_inst.get("name", "Instance")
print(f"[f0ckm-gui] Switched active instance to '{name}' ({active_inst.get('api_url', '')})", flush=True)
self.show_message("f0ckm Instance Switched", f"Active instance changed to: {name}", QSystemTrayIcon.Information)
self.rebuild_instance_menu()
if self.settings_dialog and self.settings_dialog.isVisible():
self.instance_menu.setTitle(f"Instance: {name}")
if hasattr(self, "instance_actions") and 0 <= index < len(self.instance_actions):
self.instance_actions[index].setChecked(True)
if self.settings_dialog:
self.settings_dialog.load_current_settings()
def enqueue_targets(self, targets):
@@ -1847,14 +1866,16 @@ class SystemTrayApp(QObject):
print(f"[f0ckm-gui] Notification clicked -> Opening URL in browser: '{url}'", flush=True)
QDesktopServices.openUrl(QUrl(url))
def on_open_gallery(self):
def on_open_gallery(self, checked=False):
if self.gallery_window is None:
self.gallery_window = GalleryWindow()
self.gallery_window.load_items()
if self.gallery_window.isMinimized():
self.gallery_window.showNormal()
self.gallery_window.show()
self.gallery_window.raise_()
self.gallery_window.activateWindow()
def show(self):
self.tray.show()
@@ -1889,9 +1910,12 @@ class SystemTrayApp(QObject):
self.tray.setIcon(self.default_icon)
def on_open_settings(self):
def on_open_settings(self, checked=False):
if not self.settings_dialog:
self.settings_dialog = SettingsDialog(tray_app=self)
self.settings_dialog.load_current_settings()
if self.settings_dialog.isMinimized():
self.settings_dialog.showNormal()
self.settings_dialog.show()
self.settings_dialog.raise_()
self.settings_dialog.activateWindow()