From 971f9d647eb3e5a94886c04eaee7f14f4ff7c28f Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 13 Aug 2026 19:22:04 +0200 Subject: [PATCH] fix desktop app and android app --- .../main/java/com/fuggloader/app/Instance.kt | 7 +- .../java/com/fuggloader/app/MainActivity.kt | 15 + .../java/com/fuggloader/app/ShareActivity.kt | 311 +++++++++++------- .../com/fuggloader/app/UploaderService.kt | 60 +++- .../app/src/main/res/layout/activity_main.xml | 17 + .../src/main/res/layout/activity_share.xml | 21 +- .../main/res/layout/item_upload_result.xml | 58 ++++ gui.py | 64 ++-- 8 files changed, 391 insertions(+), 162 deletions(-) create mode 100644 android/app/src/main/res/layout/item_upload_result.xml diff --git a/android/app/src/main/java/com/fuggloader/app/Instance.kt b/android/app/src/main/java/com/fuggloader/app/Instance.kt index c4cce91..f0fc8e2 100644 --- a/android/app/src/main/java/com/fuggloader/app/Instance.kt +++ b/android/app/src/main/java/com/fuggloader/app/Instance.kt @@ -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") ) } } diff --git a/android/app/src/main/java/com/fuggloader/app/MainActivity.kt b/android/app/src/main/java/com/fuggloader/app/MainActivity.kt index 44b897b..354e062 100644 --- a/android/app/src/main/java/com/fuggloader/app/MainActivity.kt +++ b/android/app/src/main/java/com/fuggloader/app/MainActivity.kt @@ -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 diff --git a/android/app/src/main/java/com/fuggloader/app/ShareActivity.kt b/android/app/src/main/java/com/fuggloader/app/ShareActivity.kt index 4cf63d6..da44fc6 100644 --- a/android/app/src/main/java/com/fuggloader/app/ShareActivity.kt +++ b/android/app/src/main/java/com/fuggloader/app/ShareActivity.kt @@ -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() + 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, + private val onAction: (UploadResult, ResultAction) -> Unit + ) : RecyclerView.Adapter() { + + 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() + 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(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(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(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(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) { + 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) - } } diff --git a/android/app/src/main/java/com/fuggloader/app/UploaderService.kt b/android/app/src/main/java/com/fuggloader/app/UploaderService.kt index ba1eb49..25d8a97 100644 --- a/android/app/src/main/java/com/fuggloader/app/UploaderService.kt +++ b/android/app/src/main/java/com/fuggloader/app/UploaderService.kt @@ -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? { + // 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") { diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index b89496c..6758468 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -190,6 +190,23 @@ android:background="#1E1E24" android:padding="8dp" /> + + + + + - -