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 apiKey: String,
var defaultRating: String = "none", var defaultRating: String = "none",
var defaultTags: String = "", var defaultTags: String = "",
var defaultVisibility: String = "0" var defaultVisibility: String = "0",
var resultLinkType: String = "post"
) { ) {
fun toJsonObject(): JSONObject { fun toJsonObject(): JSONObject {
return JSONObject().apply { return JSONObject().apply {
@@ -21,6 +22,7 @@ data class Instance(
put("defaultRating", defaultRating) put("defaultRating", defaultRating)
put("defaultTags", defaultTags) put("defaultTags", defaultTags)
put("defaultVisibility", defaultVisibility) put("defaultVisibility", defaultVisibility)
put("resultLinkType", resultLinkType)
} }
} }
@@ -35,7 +37,8 @@ data class Instance(
apiKey = json.optString("apiKey", ""), apiKey = json.optString("apiKey", ""),
defaultRating = json.optString("defaultRating", "none"), defaultRating = json.optString("defaultRating", "none"),
defaultTags = json.optString("defaultTags", ""), 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() setupRatingSpinner()
setupVisibilitySpinner() setupVisibilitySpinner()
setupLinkTypeSpinner()
setupInstanceSpinner() setupInstanceSpinner()
binding.btnSave.setOnClickListener { binding.btnSave.setOnClickListener {
@@ -63,6 +64,12 @@ class MainActivity : AppCompatActivity() {
binding.spinnerVisibility.adapter = adapter 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() { private fun setupInstanceSpinner() {
currentInstances = prefs.getInstances() currentInstances = prefs.getInstances()
if (currentInstances.isEmpty()) { if (currentInstances.isEmpty()) {
@@ -106,6 +113,12 @@ class MainActivity : AppCompatActivity() {
"2" -> binding.spinnerVisibility.setSelection(2) "2" -> binding.spinnerVisibility.setSelection(2)
else -> binding.spinnerVisibility.setSelection(0) else -> binding.spinnerVisibility.setSelection(0)
} }
if (instance.resultLinkType == "direct") {
binding.spinnerLinkType.setSelection(1)
} else {
binding.spinnerLinkType.setSelection(0)
}
} }
private fun saveSettings() { private fun saveSettings() {
@@ -125,6 +138,8 @@ class MainActivity : AppCompatActivity() {
currentInstance.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString() currentInstance.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString()
currentInstance.resultLinkType = if (binding.spinnerLinkType.selectedItemPosition == 1) "direct" else "post"
prefs.updateInstance(currentInstance) prefs.updateInstance(currentInstance)
// Refresh spinner to show updated name // Refresh spinner to show updated name

View File

@@ -6,11 +6,16 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.OpenableColumns
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity 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.ActivityShareBinding
import com.fuggloader.app.databinding.ItemUploadResultBinding
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,7 +26,10 @@ class ShareActivity : AppCompatActivity() {
private lateinit var prefs: PreferencesManager private lateinit var prefs: PreferencesManager
private lateinit var uploader: UploaderService 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -31,6 +39,8 @@ class ShareActivity : AppCompatActivity() {
prefs = PreferencesManager(this) prefs = PreferencesManager(this)
uploader = UploaderService(prefs) uploader = UploaderService(prefs)
setupRecyclerView()
if (!prefs.isConfigured()) { if (!prefs.isConfigured()) {
Toast.makeText(this, "fuggloader is not configured. Please open app settings first.", Toast.LENGTH_LONG).show() Toast.makeText(this, "fuggloader is not configured. Please open app settings first.", Toast.LENGTH_LONG).show()
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -40,17 +50,10 @@ class ShareActivity : AppCompatActivity() {
} }
binding.btnCopy.setOnClickListener { binding.btnCopy.setOnClickListener {
uploadedUrl?.let { url -> if (uploadedResults.isNotEmpty()) {
copyToClipboard(url) val text = uploadedResults.joinToString("\n") { it.url }
Toast.makeText(this, "URL copied to clipboard!", Toast.LENGTH_SHORT).show() copyToClipboard(text)
} Toast.makeText(this, "All URLs 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()
} }
} }
@@ -61,65 +64,214 @@ class ShareActivity : AppCompatActivity() {
prepareShare(intent) prepareShare(intent)
} }
private fun prepareShare(intent: Intent) { private fun setupRecyclerView() {
val instances = prefs.getInstances() resultAdapter = UploadResultAdapter(uploadedResults) { result, action ->
if (instances.size > 1) { when (action) {
val names = instances.map { it.name }.toTypedArray() ResultAction.COPY -> {
AlertDialog.Builder(this) copyToClipboard(result.url)
.setTitle("Select Instance") Toast.makeText(this, "URL copied!", Toast.LENGTH_SHORT).show()
.setItems(names) { _, which -> }
handleShareIntent(instances[which], intent) 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) { private fun handleShareIntent(instance: Instance, intent: Intent) {
val action = intent.action val action = intent.action
val type = intent.type val type = intent.type
val uris = mutableListOf<Uri>()
var sharedUrl: String? = null
if (Intent.ACTION_SEND == action && type != null) { if (Intent.ACTION_SEND == action && type != null) {
if ("text/plain" == type) { if ("text/plain" == type) {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (!sharedText.isNull_or_blank_url()) { if (!sharedText.isNull_or_blank_url()) {
val targetUrl = extractUrl(sharedText!!) sharedUrl = extractUrl(sharedText!!)
processUrlUpload(instance, targetUrl)
} else { } else {
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (streamUri != null) { if (streamUri != null) {
processFileUpload(instance, streamUri) uris.add(streamUri)
} else {
showError("No valid file or URL found in share intent")
} }
} }
} else { } else {
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (streamUri != null) { if (streamUri != null) {
processFileUpload(instance, streamUri) uris.add(streamUri)
} else {
showError("No stream URI found for file share")
} }
} }
} else if (Intent.ACTION_SEND_MULTIPLE == action) { } else if (Intent.ACTION_SEND_MULTIPLE == action) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM) val streamUris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (!uris.isNullOrEmpty()) { if (streamUris != null) {
// Upload first file in batch for now uris.addAll(streamUris)
processFileUpload(instance, uris[0])
} else {
showError("No files found in multi-share intent")
} }
} 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 { private fun String?.isNull_or_blank_url(): Boolean {
@@ -138,71 +290,4 @@ class ShareActivity : AppCompatActivity() {
} }
return trimmed 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) { if (response.isSuccessful) {
onProgress(100) onProgress(100)
val postUrl = parsePostUrl(responseBody) val postUrl = parsePostUrl(responseBody, targetInstance.resultLinkType)
Result.success(postUrl) Result.success(postUrl)
} else { } else {
Log.e("UploaderService", "Upload failed: HTTP ${response.code}, Body: $responseBody") 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() ?: "" val responseBody = response.body?.string() ?: ""
if (response.isSuccessful) { if (response.isSuccessful) {
val postUrl = parsePostUrl(responseBody) val postUrl = parsePostUrl(responseBody, targetInstance.resultLinkType)
Result.success(postUrl) Result.success(postUrl)
} else { } else {
Log.e("UploaderService", "URL Upload failed: HTTP ${response.code}, Body: $responseBody") 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 { return try {
val json = JSONObject(jsonResponse) val json = JSONObject(jsonResponse)
if (json.has("url")) {
json.getString("url") // For f0ckm API:
} else if (json.has("post_url")) { // "url" is the viewer post link (e.g. https://fuggloader.com/abcdef)
json.getString("post_url") // "file_url" is the direct link (e.g. https://fuggloader.com/b/uuid.jpg)
} else if (json.has("link")) {
json.getString("link") if (linkType == "direct") {
if (json.has("file_url") && !json.isNull("file_url")) {
return json.getString("file_url")
}
} else { } 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) { } catch (e: Exception) {
Log.e("UploaderService", "Error parsing JSON", e)
jsonResponse 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? { private fun getFileName(contentResolver: ContentResolver, uri: Uri): String? {
var result: String? = null var result: String? = null
if (uri.scheme == "content") { if (uri.scheme == "content") {

View File

@@ -190,6 +190,23 @@
android:background="#1E1E24" android:background="#1E1E24"
android:padding="8dp" /> 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 --> <!-- Buttons -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -48,15 +48,12 @@
android:textColor="#808090" android:textColor="#808090"
android:textSize="12sp" /> android:textSize="12sp" />
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/tvResultUrl" android:id="@+id/rvResults"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:background="#121216" android:maxHeight="300dp"
android:padding="8dp"
android:textColor="#A5D6A7"
android:textSize="13sp"
android:visibility="gone" /> android:visibility="gone" />
<LinearLayout <LinearLayout
@@ -69,21 +66,11 @@
<Button <Button
android:id="@+id/btnCopy" android:id="@+id/btnCopy"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:backgroundTint="#2E2E3A" android:backgroundTint="#2E2E3A"
android:text="Copy Link" /> 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> </LinearLayout>
<Button <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 http.server import HTTPServer, BaseHTTPRequestHandler
from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess 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.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout, QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
@@ -688,21 +688,29 @@ class SettingsDialog(QDialog):
self.btn_toggle_key.setText("Show") self.btn_toggle_key.setText("Show")
def refresh_instance_combo(self): def refresh_instance_combo(self):
prev_ignore = self._ignore_instance_signals
self._ignore_instance_signals = True self._ignore_instance_signals = True
self.cb_instance.clear() try:
for inst in self.instances: self.cb_instance.clear()
self.cb_instance.addItem(inst.get("name", "Unnamed Instance")) for inst in self.instances:
self.cb_instance.addItem(inst.get("name", "Unnamed Instance"))
if 0 <= self.current_instance_index < len(self.instances): if 0 <= self.current_instance_index < len(self.instances):
self.cb_instance.setCurrentIndex(self.current_instance_index) self.cb_instance.setCurrentIndex(self.current_instance_index)
self._ignore_instance_signals = False self.load_instance_fields(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): def load_instance_fields(self, index):
if 0 <= index < len(self.instances): if 0 <= index < len(self.instances):
inst = self.instances[index] prev_ignore = self._ignore_instance_signals
self.txt_url.setText(inst.get("api_url", "")) self._ignore_instance_signals = True
self.txt_key.setText(inst.get("api_key", "")) 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): def on_instance_changed(self, new_index):
if self._ignore_instance_signals or new_index < 0 or new_index >= len(self.instances): 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) 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(lambda checked=False: self.on_open_settings())
self.menu.addAction(settings_action) self.menu.addAction(settings_action)
gallery_action = QAction("Recent Uploads Gallery", self.menu) 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.addAction(gallery_action)
self.menu.addSeparator() self.menu.addSeparator()
@@ -1747,6 +1755,8 @@ class SystemTrayApp(QObject):
def rebuild_instance_menu(self): def rebuild_instance_menu(self):
self.instance_menu.clear() self.instance_menu.clear()
self.instance_actions = []
config = get_env_config() config = get_env_config()
instances = config.get("instances", []) instances = config.get("instances", [])
active_idx = config.get("active_instance_index", 0) active_idx = config.get("active_instance_index", 0)
@@ -1754,22 +1764,27 @@ class SystemTrayApp(QObject):
active_name = config.get("active_instance_name", "Default Instance") active_name = config.get("active_instance_name", "Default Instance")
self.instance_menu.setTitle(f"Instance: {active_name}") 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): for idx, inst in enumerate(instances):
name = inst.get("name", f"Instance {idx + 1}") name = inst.get("name", f"Instance {idx + 1}")
url = inst.get("api_url", "") url = inst.get("api_url", "")
action = QAction(name, self.instance_menu) action = QAction(name, self.instance_menu)
action.setCheckable(True)
self.instance_action_group.addAction(action)
if url: if url:
action.setToolTip(url) action.setToolTip(url)
action.setCheckable(True)
if idx == active_idx: if idx == active_idx:
action.setChecked(True) action.setChecked(True)
action.triggered.connect(lambda checked=False, i=idx: self.switch_active_instance(i)) action.triggered.connect(lambda checked=False, i=idx: self.switch_active_instance(i))
self.instance_menu.addAction(action) self.instance_menu.addAction(action)
self.instance_actions.append(action)
self.instance_menu.addSeparator() self.instance_menu.addSeparator()
manage_action = QAction("Manage Instances...", self.instance_menu) 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) self.instance_menu.addAction(manage_action)
def switch_active_instance(self, index): def switch_active_instance(self, index):
@@ -1785,8 +1800,12 @@ class SystemTrayApp(QObject):
name = active_inst.get("name", "Instance") name = active_inst.get("name", "Instance")
print(f"[f0ckm-gui] Switched active instance to '{name}' ({active_inst.get('api_url', '')})", flush=True) 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.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() self.settings_dialog.load_current_settings()
def enqueue_targets(self, targets): 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) print(f"[f0ckm-gui] Notification clicked -> Opening URL in browser: '{url}'", flush=True)
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))
def on_open_gallery(self): def on_open_gallery(self, checked=False):
if self.gallery_window is None: if self.gallery_window is None:
self.gallery_window = GalleryWindow() self.gallery_window = GalleryWindow()
self.gallery_window.load_items() self.gallery_window.load_items()
if self.gallery_window.isMinimized():
self.gallery_window.showNormal()
self.gallery_window.show() self.gallery_window.show()
self.gallery_window.raise_() self.gallery_window.raise_()
self.gallery_window.activateWindow() self.gallery_window.activateWindow()
def show(self): def show(self):
self.tray.show() self.tray.show()
@@ -1889,9 +1910,12 @@ class SystemTrayApp(QObject):
self.tray.setIcon(self.default_icon) self.tray.setIcon(self.default_icon)
def on_open_settings(self): def on_open_settings(self, checked=False):
if not self.settings_dialog: if not self.settings_dialog:
self.settings_dialog = SettingsDialog(tray_app=self) 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.show()
self.settings_dialog.raise_() self.settings_dialog.raise_()
self.settings_dialog.activateWindow() self.settings_dialog.activateWindow()