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>