update browser extension and app
This commit is contained in:
42
android/app/src/main/java/com/fuggloader/app/Instance.kt
Normal file
42
android/app/src/main/java/com/fuggloader/app/Instance.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package com.fuggloader.app
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class Instance(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
var name: String,
|
||||||
|
var apiUrl: String,
|
||||||
|
var apiKey: String,
|
||||||
|
var defaultRating: String = "none",
|
||||||
|
var defaultTags: String = "",
|
||||||
|
var defaultVisibility: String = "0"
|
||||||
|
) {
|
||||||
|
fun toJsonObject(): JSONObject {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("id", id)
|
||||||
|
put("name", name)
|
||||||
|
put("apiUrl", apiUrl)
|
||||||
|
put("apiKey", apiKey)
|
||||||
|
put("defaultRating", defaultRating)
|
||||||
|
put("defaultTags", defaultTags)
|
||||||
|
put("defaultVisibility", defaultVisibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = name
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJsonObject(json: JSONObject): Instance {
|
||||||
|
return Instance(
|
||||||
|
id = json.optString("id", UUID.randomUUID().toString()),
|
||||||
|
name = json.optString("name", "Default"),
|
||||||
|
apiUrl = json.optString("apiUrl", ""),
|
||||||
|
apiKey = json.optString("apiKey", ""),
|
||||||
|
defaultRating = json.optString("defaultRating", "none"),
|
||||||
|
defaultTags = json.optString("defaultTags", ""),
|
||||||
|
defaultVisibility = json.optString("defaultVisibility", "0")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.fuggloader.app
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -9,7 +11,6 @@ import com.fuggloader.app.databinding.ActivityMainBinding
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private lateinit var prefs: PreferencesManager
|
private lateinit var prefs: PreferencesManager
|
||||||
private lateinit var uploader: UploaderService
|
private lateinit var uploader: UploaderService
|
||||||
|
|
||||||
|
private lateinit var instanceAdapter: ArrayAdapter<Instance>
|
||||||
|
private var currentInstances: List<Instance> = emptyList()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
@@ -27,7 +31,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
setupRatingSpinner()
|
setupRatingSpinner()
|
||||||
setupVisibilitySpinner()
|
setupVisibilitySpinner()
|
||||||
loadSettings()
|
setupInstanceSpinner()
|
||||||
|
|
||||||
binding.btnSave.setOnClickListener {
|
binding.btnSave.setOnClickListener {
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -35,9 +39,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnTestConnection.setOnClickListener {
|
binding.btnTestConnection.setOnClickListener {
|
||||||
saveSettings()
|
|
||||||
testConnection()
|
testConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnAddInstance.setOnClickListener {
|
||||||
|
addNewInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnDeleteInstance.setOnClickListener {
|
||||||
|
deleteCurrentInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRatingSpinner() {
|
private fun setupRatingSpinner() {
|
||||||
@@ -52,19 +63,45 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.spinnerVisibility.adapter = adapter
|
binding.spinnerVisibility.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun setupInstanceSpinner() {
|
||||||
binding.etApiUrl.setText(prefs.apiUrl)
|
currentInstances = prefs.getInstances()
|
||||||
binding.etApiKey.setText(prefs.apiKey)
|
if (currentInstances.isEmpty()) {
|
||||||
binding.etTags.setText(prefs.defaultTags)
|
val default = Instance(name = "Default", apiUrl = "", apiKey = "")
|
||||||
|
prefs.addInstance(default)
|
||||||
|
currentInstances = prefs.getInstances()
|
||||||
|
}
|
||||||
|
|
||||||
when (prefs.defaultRating) {
|
instanceAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, currentInstances)
|
||||||
|
binding.spinnerInstances.adapter = instanceAdapter
|
||||||
|
|
||||||
|
val activeId = prefs.activeInstanceId
|
||||||
|
val activeIndex = currentInstances.indexOfFirst { it.id == activeId }.coerceAtLeast(0)
|
||||||
|
binding.spinnerInstances.setSelection(activeIndex)
|
||||||
|
|
||||||
|
binding.spinnerInstances.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
val selected = currentInstances[position]
|
||||||
|
prefs.activeInstanceId = selected.id
|
||||||
|
loadInstanceData(selected)
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadInstanceData(instance: Instance) {
|
||||||
|
binding.etInstanceName.setText(instance.name)
|
||||||
|
binding.etApiUrl.setText(instance.apiUrl)
|
||||||
|
binding.etApiKey.setText(instance.apiKey)
|
||||||
|
binding.etTags.setText(instance.defaultTags)
|
||||||
|
|
||||||
|
when (instance.defaultRating) {
|
||||||
"sfw" -> binding.spinnerRating.setSelection(1)
|
"sfw" -> binding.spinnerRating.setSelection(1)
|
||||||
"nsfw" -> binding.spinnerRating.setSelection(2)
|
"nsfw" -> binding.spinnerRating.setSelection(2)
|
||||||
"nsfl" -> binding.spinnerRating.setSelection(3)
|
"nsfl" -> binding.spinnerRating.setSelection(3)
|
||||||
else -> binding.spinnerRating.setSelection(0)
|
else -> binding.spinnerRating.setSelection(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (prefs.defaultVisibility) {
|
when (instance.defaultVisibility) {
|
||||||
"1" -> binding.spinnerVisibility.setSelection(1)
|
"1" -> binding.spinnerVisibility.setSelection(1)
|
||||||
"2" -> binding.spinnerVisibility.setSelection(2)
|
"2" -> binding.spinnerVisibility.setSelection(2)
|
||||||
else -> binding.spinnerVisibility.setSelection(0)
|
else -> binding.spinnerVisibility.setSelection(0)
|
||||||
@@ -72,22 +109,54 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings() {
|
private fun saveSettings() {
|
||||||
prefs.apiUrl = binding.etApiUrl.text.toString().trim()
|
val currentInstance = prefs.getActiveInstance() ?: return
|
||||||
prefs.apiKey = binding.etApiKey.text.toString().trim()
|
|
||||||
prefs.defaultTags = binding.etTags.text.toString().trim()
|
|
||||||
|
|
||||||
prefs.defaultRating = when (binding.spinnerRating.selectedItemPosition) {
|
currentInstance.name = binding.etInstanceName.text.toString().trim().ifBlank { "Untitled" }
|
||||||
|
currentInstance.apiUrl = binding.etApiUrl.text.toString().trim()
|
||||||
|
currentInstance.apiKey = binding.etApiKey.text.toString().trim()
|
||||||
|
currentInstance.defaultTags = binding.etTags.text.toString().trim()
|
||||||
|
|
||||||
|
currentInstance.defaultRating = when (binding.spinnerRating.selectedItemPosition) {
|
||||||
1 -> "sfw"
|
1 -> "sfw"
|
||||||
2 -> "nsfw"
|
2 -> "nsfw"
|
||||||
3 -> "nsfl"
|
3 -> "nsfl"
|
||||||
else -> "none"
|
else -> "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString()
|
currentInstance.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString()
|
||||||
|
|
||||||
|
prefs.updateInstance(currentInstance)
|
||||||
|
|
||||||
|
// Refresh spinner to show updated name
|
||||||
|
val pos = binding.spinnerInstances.selectedItemPosition
|
||||||
|
currentInstances = prefs.getInstances()
|
||||||
|
instanceAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, currentInstances)
|
||||||
|
binding.spinnerInstances.adapter = instanceAdapter
|
||||||
|
binding.spinnerInstances.setSelection(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addNewInstance() {
|
||||||
|
val newInstance = Instance(name = "New Instance", apiUrl = "", apiKey = "")
|
||||||
|
prefs.addInstance(newInstance)
|
||||||
|
prefs.activeInstanceId = newInstance.id
|
||||||
|
setupInstanceSpinner()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteCurrentInstance() {
|
||||||
|
val current = prefs.getActiveInstance() ?: return
|
||||||
|
if (currentInstances.size <= 1) {
|
||||||
|
Toast.makeText(this, "Cannot delete the only instance", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.deleteInstance(current.id)
|
||||||
|
setupInstanceSpinner()
|
||||||
|
Toast.makeText(this, "Instance deleted", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testConnection() {
|
private fun testConnection() {
|
||||||
if (!prefs.isConfigured()) {
|
val currentInstance = prefs.getActiveInstance()
|
||||||
|
if (currentInstance == null || currentInstance.apiUrl.isBlank() || currentInstance.apiKey.isBlank()) {
|
||||||
Toast.makeText(this, "Please enter both API URL and API Key", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Please enter both API URL and API Key", Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,7 +165,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.btnTestConnection.text = "Testing..."
|
binding.btnTestConnection.text = "Testing..."
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val result = uploader.testConnection()
|
val result = uploader.testConnection(currentInstance)
|
||||||
binding.btnTestConnection.isEnabled = true
|
binding.btnTestConnection.isEnabled = true
|
||||||
binding.btnTestConnection.text = "Test"
|
binding.btnTestConnection.text = "Test"
|
||||||
|
|
||||||
|
|||||||
@@ -2,31 +2,112 @@ package com.fuggloader.app
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import org.json.JSONArray
|
||||||
|
|
||||||
class PreferencesManager(context: Context) {
|
class PreferencesManager(context: Context) {
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("fuggloader_prefs", Context.MODE_PRIVATE)
|
private val prefs: SharedPreferences = context.getSharedPreferences("fuggloader_prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
var apiUrl: String
|
companion object {
|
||||||
get() = prefs.getString("api_url", "") ?: ""
|
private const val KEY_INSTANCES = "instances_json"
|
||||||
set(value) = prefs.edit().putString("api_url", value).apply()
|
private const val KEY_ACTIVE_INSTANCE_ID = "active_instance_id"
|
||||||
|
|
||||||
var apiKey: String
|
// Old keys for migration
|
||||||
get() = prefs.getString("api_key", "") ?: ""
|
private const val OLD_API_URL = "api_url"
|
||||||
set(value) = prefs.edit().putString("api_key", value).apply()
|
private const val OLD_API_KEY = "api_key"
|
||||||
|
private const val OLD_RATING = "default_rating"
|
||||||
|
private const val OLD_TAGS = "default_tags"
|
||||||
|
private const val OLD_VISIBILITY = "default_visibility"
|
||||||
|
}
|
||||||
|
|
||||||
var defaultRating: String
|
var activeInstanceId: String?
|
||||||
get() = prefs.getString("default_rating", "none") ?: "none"
|
get() = prefs.getString(KEY_ACTIVE_INSTANCE_ID, null)
|
||||||
set(value) = prefs.edit().putString("default_rating", value).apply()
|
set(value) = prefs.edit().putString(KEY_ACTIVE_INSTANCE_ID, value).apply()
|
||||||
|
|
||||||
var defaultTags: String
|
fun getInstances(): List<Instance> {
|
||||||
get() = prefs.getString("default_tags", "android,upload") ?: "android,upload"
|
val jsonStr = prefs.getString(KEY_INSTANCES, null)
|
||||||
set(value) = prefs.edit().putString("default_tags", value).apply()
|
if (jsonStr == null) {
|
||||||
|
// Check for migration
|
||||||
|
val oldUrl = prefs.getString(OLD_API_URL, null)
|
||||||
|
if (oldUrl != null) {
|
||||||
|
val instance = Instance(
|
||||||
|
name = "Default",
|
||||||
|
apiUrl = oldUrl,
|
||||||
|
apiKey = prefs.getString(OLD_API_KEY, "") ?: "",
|
||||||
|
defaultRating = prefs.getString(OLD_RATING, "none") ?: "none",
|
||||||
|
defaultTags = prefs.getString(OLD_TAGS, "android,upload") ?: "android,upload",
|
||||||
|
defaultVisibility = prefs.getString(OLD_VISIBILITY, "0") ?: "0"
|
||||||
|
)
|
||||||
|
val list = listOf(instance)
|
||||||
|
saveInstances(list)
|
||||||
|
activeInstanceId = instance.id
|
||||||
|
|
||||||
var defaultVisibility: String
|
// Clear old settings to avoid re-migration
|
||||||
get() = prefs.getString("default_visibility", "0") ?: "0"
|
prefs.edit()
|
||||||
set(value) = prefs.edit().putString("default_visibility", value).apply()
|
.remove(OLD_API_URL)
|
||||||
|
.remove(OLD_API_KEY)
|
||||||
|
.remove(OLD_RATING)
|
||||||
|
.remove(OLD_TAGS)
|
||||||
|
.remove(OLD_VISIBILITY)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val list = mutableListOf<Instance>()
|
||||||
|
try {
|
||||||
|
val array = JSONArray(jsonStr)
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
list.add(Instance.fromJsonObject(array.getJSONObject(i)))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveInstances(instances: List<Instance>) {
|
||||||
|
val array = JSONArray()
|
||||||
|
instances.forEach { array.put(it.toJsonObject()) }
|
||||||
|
prefs.edit().putString(KEY_INSTANCES, array.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addInstance(instance: Instance) {
|
||||||
|
val instances = getInstances().toMutableList()
|
||||||
|
instances.add(instance)
|
||||||
|
saveInstances(instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInstance(instance: Instance) {
|
||||||
|
val instances = getInstances().toMutableList()
|
||||||
|
val index = instances.indexOfFirst { it.id == instance.id }
|
||||||
|
if (index != -1) {
|
||||||
|
instances[index] = instance
|
||||||
|
saveInstances(instances)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteInstance(id: String) {
|
||||||
|
val instances = getInstances().toMutableList()
|
||||||
|
instances.removeAll { it.id == id }
|
||||||
|
saveInstances(instances)
|
||||||
|
if (activeInstanceId == id) {
|
||||||
|
activeInstanceId = instances.firstOrNull()?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveInstance(): Instance? {
|
||||||
|
val instances = getInstances()
|
||||||
|
if (instances.isEmpty()) return null
|
||||||
|
|
||||||
|
val id = activeInstanceId ?: return instances.firstOrNull()
|
||||||
|
return instances.find { it.id == id } ?: instances.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
fun isConfigured(): Boolean {
|
fun isConfigured(): Boolean {
|
||||||
return apiUrl.isNotBlank() && apiKey.isNotBlank()
|
return getActiveInstance()?.let {
|
||||||
|
it.apiUrl.isNotBlank() && it.apiKey.isNotBlank()
|
||||||
|
} ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.fuggloader.app.databinding.ActivityShareBinding
|
import com.fuggloader.app.databinding.ActivityShareBinding
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -57,10 +58,32 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShareIntent(intent)
|
prepareShare(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShareIntent(intent: 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)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { _, _ -> finish() }
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
val target = instances.firstOrNull()
|
||||||
|
if (target != null) {
|
||||||
|
handleShareIntent(target, intent)
|
||||||
|
} else {
|
||||||
|
showError("No instance configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShareIntent(instance: Instance, intent: Intent) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
val type = intent.type
|
val type = intent.type
|
||||||
|
|
||||||
@@ -69,11 +92,11 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
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!!)
|
val targetUrl = extractUrl(sharedText!!)
|
||||||
processUrlUpload(targetUrl)
|
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(streamUri)
|
processFileUpload(instance, streamUri)
|
||||||
} else {
|
} else {
|
||||||
showError("No valid file or URL found in share intent")
|
showError("No valid file or URL found in share intent")
|
||||||
}
|
}
|
||||||
@@ -81,7 +104,7 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
} 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(streamUri)
|
processFileUpload(instance, streamUri)
|
||||||
} else {
|
} else {
|
||||||
showError("No stream URI found for file share")
|
showError("No stream URI found for file share")
|
||||||
}
|
}
|
||||||
@@ -90,7 +113,7 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
||||||
if (!uris.isNullOrEmpty()) {
|
if (!uris.isNullOrEmpty()) {
|
||||||
// Upload first file in batch for now
|
// Upload first file in batch for now
|
||||||
processFileUpload(uris[0])
|
processFileUpload(instance, uris[0])
|
||||||
} else {
|
} else {
|
||||||
showError("No files found in multi-share intent")
|
showError("No files found in multi-share intent")
|
||||||
}
|
}
|
||||||
@@ -116,8 +139,8 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFileUpload(uri: Uri) {
|
private fun processFileUpload(instance: Instance, uri: Uri) {
|
||||||
binding.tvStatus.text = "Uploading file..."
|
binding.tvStatus.text = "Uploading to ${instance.name}..."
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
binding.progressBar.isIndeterminate = false
|
binding.progressBar.isIndeterminate = false
|
||||||
binding.progressBar.progress = 0
|
binding.progressBar.progress = 0
|
||||||
@@ -126,6 +149,7 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
val result = uploader.uploadFile(
|
val result = uploader.uploadFile(
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
instance = instance,
|
||||||
onProgress = { pct ->
|
onProgress = { pct ->
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
binding.progressBar.progress = pct
|
binding.progressBar.progress = pct
|
||||||
@@ -141,14 +165,14 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processUrlUpload(url: String) {
|
private fun processUrlUpload(instance: Instance, url: String) {
|
||||||
binding.tvStatus.text = "Uploading URL..."
|
binding.tvStatus.text = "Uploading to ${instance.name}..."
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
binding.progressBar.isIndeterminate = true
|
binding.progressBar.isIndeterminate = true
|
||||||
binding.tvPercent.text = ""
|
binding.tvPercent.text = ""
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val result = uploader.uploadUrl(url)
|
val result = uploader.uploadUrl(url, instance)
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { postUrl -> showSuccess(postUrl) },
|
onSuccess = { postUrl -> showSuccess(postUrl) },
|
||||||
onFailure = { err -> showError(err.message ?: "URL Upload failed") }
|
onFailure = { err -> showError(err.message ?: "URL Upload failed") }
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ class UploaderService(private val prefs: PreferencesManager) {
|
|||||||
suspend fun uploadFile(
|
suspend fun uploadFile(
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
rating: String = prefs.defaultRating,
|
instance: Instance? = null,
|
||||||
tags: String = prefs.defaultTags,
|
|
||||||
visibility: String = prefs.defaultVisibility,
|
|
||||||
onProgress: (percent: Int) -> Unit
|
onProgress: (percent: Int) -> Unit
|
||||||
): Result<String> = withContext(Dispatchers.IO) {
|
): Result<String> = withContext(Dispatchers.IO) {
|
||||||
Log.d("UploaderService", "Starting uploadFile for $uri")
|
val targetInstance = instance ?: prefs.getActiveInstance()
|
||||||
|
?: return@withContext Result.failure(Exception("No instance configured"))
|
||||||
|
|
||||||
|
Log.d("UploaderService", "Starting uploadFile for $uri to ${targetInstance.apiUrl}")
|
||||||
try {
|
try {
|
||||||
val fileName = getFileName(contentResolver, uri) ?: "upload_${System.currentTimeMillis()}"
|
val fileName = getFileName(contentResolver, uri) ?: "upload_${System.currentTimeMillis()}"
|
||||||
val mimeType = contentResolver.getType(uri) ?: "application/octet-stream"
|
val mimeType = contentResolver.getType(uri) ?: "application/octet-stream"
|
||||||
@@ -69,16 +70,16 @@ class UploaderService(private val prefs: PreferencesManager) {
|
|||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("file", fileName, requestBody)
|
.addFormDataPart("file", fileName, requestBody)
|
||||||
|
|
||||||
if (rating != "none") {
|
if (targetInstance.defaultRating != "none") {
|
||||||
multipartBuilder.addFormDataPart("rating", rating)
|
multipartBuilder.addFormDataPart("rating", targetInstance.defaultRating)
|
||||||
}
|
}
|
||||||
|
|
||||||
multipartBuilder.addFormDataPart("tags", tags)
|
multipartBuilder.addFormDataPart("tags", targetInstance.defaultTags)
|
||||||
.addFormDataPart("visibility", visibility)
|
.addFormDataPart("visibility", targetInstance.defaultVisibility)
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(prefs.apiUrl)
|
.url(targetInstance.apiUrl)
|
||||||
.addHeader("X-Api-Key", prefs.apiKey)
|
.addHeader("X-Api-Key", targetInstance.apiKey)
|
||||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||||
.post(multipartBuilder.build())
|
.post(multipartBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
@@ -101,19 +102,20 @@ class UploaderService(private val prefs: PreferencesManager) {
|
|||||||
|
|
||||||
suspend fun uploadUrl(
|
suspend fun uploadUrl(
|
||||||
url: String,
|
url: String,
|
||||||
rating: String = prefs.defaultRating,
|
instance: Instance? = null
|
||||||
tags: String = prefs.defaultTags,
|
|
||||||
visibility: String = prefs.defaultVisibility
|
|
||||||
): Result<String> = withContext(Dispatchers.IO) {
|
): Result<String> = withContext(Dispatchers.IO) {
|
||||||
Log.d("UploaderService", "Starting uploadUrl for $url")
|
val targetInstance = instance ?: prefs.getActiveInstance()
|
||||||
|
?: return@withContext Result.failure(Exception("No instance configured"))
|
||||||
|
|
||||||
|
Log.d("UploaderService", "Starting uploadUrl for $url to ${targetInstance.apiUrl}")
|
||||||
try {
|
try {
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("url", url)
|
put("url", url)
|
||||||
if (rating != "none") {
|
if (targetInstance.defaultRating != "none") {
|
||||||
put("rating", rating)
|
put("rating", targetInstance.defaultRating)
|
||||||
}
|
}
|
||||||
put("tags", tags)
|
put("tags", targetInstance.defaultTags)
|
||||||
put("visibility", visibility)
|
put("visibility", targetInstance.defaultVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = RequestBody.create(
|
val requestBody = RequestBody.create(
|
||||||
@@ -122,8 +124,8 @@ class UploaderService(private val prefs: PreferencesManager) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(prefs.apiUrl)
|
.url(targetInstance.apiUrl)
|
||||||
.addHeader("X-Api-Key", prefs.apiKey)
|
.addHeader("X-Api-Key", targetInstance.apiKey)
|
||||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build()
|
.build()
|
||||||
@@ -143,12 +145,15 @@ class UploaderService(private val prefs: PreferencesManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun testConnection(): Result<String> = withContext(Dispatchers.IO) {
|
suspend fun testConnection(instance: Instance? = null): Result<String> = withContext(Dispatchers.IO) {
|
||||||
Log.d("UploaderService", "Starting testConnection ping")
|
val targetInstance = instance ?: prefs.getActiveInstance()
|
||||||
|
?: return@withContext Result.failure(Exception("No instance configured"))
|
||||||
|
|
||||||
|
Log.d("UploaderService", "Starting testConnection ping for ${targetInstance.apiUrl}")
|
||||||
try {
|
try {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(prefs.apiUrl)
|
.url(targetInstance.apiUrl)
|
||||||
.addHeader("X-Api-Key", prefs.apiKey)
|
.addHeader("X-Api-Key", targetInstance.apiKey)
|
||||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||||
.post(ByteArray(0).toRequestBody(null))
|
.post(ByteArray(0).toRequestBody(null))
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -28,11 +28,76 @@
|
|||||||
android:textColor="#A0A0B0"
|
android:textColor="#A0A0B0"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<!-- Instance Selection -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="Select Instance"
|
||||||
|
android:textColor="#E0E0E0"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerInstances"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="#1E1E24" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnAddInstance"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="#1E1E24"
|
||||||
|
android:contentDescription="Add Instance"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:tint="#FFFFFF" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnDeleteInstance"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="#1E1E24"
|
||||||
|
android:contentDescription="Delete Instance"
|
||||||
|
android:src="@android:drawable/ic_menu_delete"
|
||||||
|
app:tint="#FF4444" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Instance Name -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Instance Name"
|
||||||
|
android:textColor="#E0E0E0"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etInstanceName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="#1E1E24"
|
||||||
|
android:hint="My Server"
|
||||||
|
android:inputType="text"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textColorHint="#606070" />
|
||||||
|
|
||||||
<!-- API Endpoint URL -->
|
<!-- API Endpoint URL -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="API Endpoint URL"
|
android:text="API Endpoint URL"
|
||||||
android:textColor="#E0E0E0"
|
android:textColor="#E0E0E0"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|||||||
@@ -25,19 +25,67 @@ A ShareX-style browser extension for **Chrome, Brave, Edge, Opera, and Firefox**
|
|||||||
|
|
||||||
## Installation Instructions
|
## Installation Instructions
|
||||||
|
|
||||||
### Chrome, Brave, Edge, Opera (Chromium)
|
### Chrome, Brave, Edge, Opera (Chromium) — Permanent by default
|
||||||
|
|
||||||
1. Open your browser and navigate to `chrome://extensions` (or `edge://extensions` in Edge).
|
1. Open your browser and navigate to `chrome://extensions` (or `edge://extensions` in Edge).
|
||||||
2. Enable **Developer mode** (toggle in the top-right corner).
|
2. Enable **Developer mode** (toggle switch in top-right).
|
||||||
3. Click **Load unpacked** in the top-left menu.
|
3. Click **Load unpacked**.
|
||||||
4. Select the `browser-extension` folder in this repository (`/home/kibi/Projects/f0ckm-uploader/browser-extension`).
|
4. Select the `browser-extension` folder in this repository (`/home/kibi/Projects/f0ckm-uploader/browser-extension`).
|
||||||
5. Pin the **f0ckm Uploader** icon to your toolbar for easy access!
|
5. *Note*: In Chromium browsers, loading unpacked extensions **remains installed permanently** across browser restarts as long as the folder location is not moved or deleted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Mozilla Firefox
|
### Mozilla Firefox
|
||||||
|
|
||||||
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`.
|
Firefox restricts unsigned extensions from being permanently installed in standard release builds by default. Choose one of the following methods for permanent installation:
|
||||||
|
|
||||||
|
#### Method 1: System Policy (`policies.json`) — Recommended for standard Firefox on Linux
|
||||||
|
|
||||||
|
1. Zip the extension directory:
|
||||||
|
```bash
|
||||||
|
cd /home/kibi/Projects/f0ckm-uploader/browser-extension
|
||||||
|
zip -r /tmp/f0ckm-uploader.xpi *
|
||||||
|
```
|
||||||
|
2. Create or edit `/etc/firefox/policies/policies.json` with superuser privileges:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"policies": {
|
||||||
|
"ExtensionSettings": {
|
||||||
|
"f0ckm-uploader@local": {
|
||||||
|
"installation_mode": "force_installed",
|
||||||
|
"install_url": "file:///tmp/f0ckm-uploader.xpi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Restart Firefox. The extension will be permanently installed.
|
||||||
|
|
||||||
|
#### Method 2: Firefox Developer Edition / Nightly / ESR / LibreWolf / Waterfox
|
||||||
|
|
||||||
|
1. Open `about:config` in the address bar and accept the risk.
|
||||||
|
2. Search for `xpinstall.signatures.required` and toggle it to **`false`**.
|
||||||
|
3. Package the extension as `.xpi`:
|
||||||
|
```bash
|
||||||
|
cd /home/kibi/Projects/f0ckm-uploader/browser-extension
|
||||||
|
zip -r ~/f0ckm-uploader.xpi *
|
||||||
|
```
|
||||||
|
4. Open `about:addons`, click the gear icon (⚙️) -> **Install Add-on From File...** and select `f0ckm-uploader.xpi`.
|
||||||
|
|
||||||
|
#### Method 3: Self-Signing via Mozilla Add-ons Hub (AMO) — Works on standard Firefox release
|
||||||
|
|
||||||
|
1. Zip the `browser-extension` directory contents.
|
||||||
|
2. Go to the [Mozilla Add-on Developer Hub](https://addons.mozilla.org/en-US/developers/).
|
||||||
|
3. Submit a new add-on and choose **"On your own"** (Self-distribution / unlisted).
|
||||||
|
4. Mozilla's automated scanner will sign your `.xpi` file within a minute.
|
||||||
|
5. Download your signed `.xpi` file and open it in Firefox (`Ctrl+O` or drag and drop into `about:addons`) to install permanently.
|
||||||
|
|
||||||
|
#### Method 4: Temporary Installation (Development mode)
|
||||||
|
|
||||||
|
1. Navigate to `about:debugging#/runtime/this-firefox`.
|
||||||
2. Click **Load Temporary Add-on...**
|
2. Click **Load Temporary Add-on...**
|
||||||
3. Select `manifest.json` from the `browser-extension` folder.
|
3. Select `manifest.json` from the `browser-extension` folder.
|
||||||
|
*(Note: Temporary add-ons automatically unload when Firefox closes).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,73 @@ chrome.commands.onCommand.addListener(async (command) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function normalizeApiUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
url = url.trim().replace(/^['"]|['"]$/g, "");
|
||||||
|
if (!url) return "";
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
url = "http://" + url;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
let path = parsed.pathname.replace(/\/+$/, "");
|
||||||
|
if (!path || path === "") {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
} else if (path.endsWith("/api/v2")) {
|
||||||
|
return `${parsed.origin}/upload`;
|
||||||
|
} else if (!path.endsWith("/upload")) {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(text) {
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testConnection(apiUrl, apiKey) {
|
||||||
|
if (!apiUrl || !apiKey) {
|
||||||
|
return { success: false, msg: "API URL and API Key are required." };
|
||||||
|
}
|
||||||
|
const normUrl = normalizeApiUrl(apiUrl);
|
||||||
|
try {
|
||||||
|
const res = await fetch(normUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": apiKey,
|
||||||
|
"Authorization": `Bearer ${apiKey}`
|
||||||
|
},
|
||||||
|
credentials: "omit",
|
||||||
|
body: JSON.stringify({ api_key: apiKey, key: apiKey })
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
return { success: true, msg: "Connection successful! (HTTP 200)" };
|
||||||
|
} else if (res.status === 401) {
|
||||||
|
return { success: false, msg: "Unauthorized: Invalid API Key." };
|
||||||
|
} else if (res.status === 400 || res.status === 422) {
|
||||||
|
return { success: true, msg: "Connection successful! (API key verified)" };
|
||||||
|
} else {
|
||||||
|
let text = `HTTP ${res.status}`;
|
||||||
|
try {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.msg || json.error || json.message) text = json.msg || json.error || json.message;
|
||||||
|
} catch (e) {}
|
||||||
|
return { success: false, msg: text };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, msg: err.message || "Connection failed. Check server URL and network." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function processUpload(targetUrl) {
|
async function processUpload(targetUrl) {
|
||||||
if (!targetUrl) return;
|
if (!targetUrl) return;
|
||||||
|
|
||||||
@@ -160,45 +227,69 @@ async function processUpload(targetUrl) {
|
|||||||
try {
|
try {
|
||||||
if (!settings.apiUrl || !settings.apiKey) {
|
if (!settings.apiUrl || !settings.apiKey) {
|
||||||
showNotification("Upload Error", "API URL and API Key must be configured in extension options.", "error");
|
showNotification("Upload Error", "API URL and API Key must be configured in extension options.", "error");
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedUrl = normalizeApiUrl(settings.apiUrl);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
tags: settings.tags || "url,upload",
|
tags: settings.tags || "url,upload",
|
||||||
visibility: settings.visibility || "0"
|
visibility: settings.visibility || "0",
|
||||||
|
api_key: settings.apiKey,
|
||||||
|
key: settings.apiKey
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settings.rating && settings.rating !== "default") {
|
if (settings.rating && settings.rating !== "default" && settings.rating !== "") {
|
||||||
const ratingMap = { s: "sfw", q: "nsfw", e: "nsfl", sfw: "sfw", nsfw: "nsfw", nsfl: "nsfl" };
|
const ratingMap = { s: "sfw", q: "nsfw", e: "nsfl", sfw: "sfw", nsfw: "nsfw", nsfl: "nsfl" };
|
||||||
payload.rating = ratingMap[settings.rating.toLowerCase()] || settings.rating;
|
payload.rating = ratingMap[settings.rating.toLowerCase()] || settings.rating;
|
||||||
}
|
}
|
||||||
if (settings.isOc) payload.is_oc = "1";
|
if (settings.isOc) payload.is_oc = "1";
|
||||||
|
|
||||||
const uploadRes = await fetch(settings.apiUrl, {
|
const uploadRes = await fetch(normalizedUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Api-Key": settings.apiKey
|
"X-Api-Key": settings.apiKey,
|
||||||
|
"Authorization": `Bearer ${settings.apiKey}`
|
||||||
},
|
},
|
||||||
|
credentials: "omit",
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!uploadRes.ok) {
|
||||||
|
let errorMsg = `HTTP Error ${uploadRes.status}`;
|
||||||
|
try {
|
||||||
|
const errJson = await uploadRes.json();
|
||||||
|
if (errJson.msg || errJson.error || errJson.message) {
|
||||||
|
errorMsg = errJson.msg || errJson.error || errJson.message;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
showNotification("Upload Failed", errorMsg, "error");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const json = await uploadRes.json();
|
const json = await uploadRes.json();
|
||||||
|
|
||||||
if (json.success && (json.url || json.file_url || json.direct_url || json.target_url || json.file)) {
|
const postUrl = json.url || json.post_url || json.link || (json.data && (json.data.url || json.data.post_url)) || "";
|
||||||
const postUrl = json.url || json.post_url || "";
|
const directUrl = json.file_url || json.direct_url || json.target_url || json.file || (json.data && (json.data.file_url || json.data.direct_url)) || "";
|
||||||
const directUrl = json.file_url || json.direct_url || json.target_url || json.file || "";
|
const resultUrl = (settings.urlType === "direct" && directUrl) ? directUrl : (postUrl || directUrl);
|
||||||
const resultUrl = (settings.urlType === "direct" && directUrl) ? directUrl : (postUrl || directUrl);
|
|
||||||
|
|
||||||
showNotification("Upload Successful!", `Link: ${resultUrl}`, "success");
|
if (resultUrl) {
|
||||||
|
await copyToClipboard(resultUrl);
|
||||||
|
showNotification("Upload Successful!", `Link copied to clipboard: ${resultUrl}`, "success");
|
||||||
return resultUrl;
|
return resultUrl;
|
||||||
|
} else if (json.success) {
|
||||||
|
showNotification("Upload Successful!", "Upload processed successfully.", "success");
|
||||||
|
return "success";
|
||||||
} else {
|
} else {
|
||||||
const msg = json.msg || "Upload failed";
|
const msg = json.msg || json.error || "Upload failed";
|
||||||
showNotification("Upload Failed", msg, "error");
|
showNotification("Upload Failed", msg, "error");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotification("Upload Error", err.message || "An error occurred", "error");
|
showNotification("Upload Error", err.message || "An error occurred during network request", "error");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,5 +329,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
sendResponse({ status: "ok", resultUrl: resUrl || "" });
|
sendResponse({ status: "ok", resultUrl: resUrl || "" });
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
} else if (request.action === "test_connection") {
|
||||||
|
testConnection(request.apiUrl, request.apiKey).then((res) => {
|
||||||
|
sendResponse(res);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "f0ckm Uploader",
|
"name": "f0ckm Uploader",
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "f0ckm-uploader@local",
|
||||||
|
"strict_min_version": "109.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"description": "Upload images, media, links, and URLs directly to f0ckm from context menu or toolbar.",
|
"description": "Upload images, media, links, and URLs directly to f0ckm from context menu or toolbar.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
<label for="apiKey">API Key</label>
|
<label for="apiKey">API Key</label>
|
||||||
<input type="password" id="apiKey" placeholder="Enter your secret API key">
|
<input type="password" id="apiKey" placeholder="Enter your secret API key">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field-row" style="margin-top: 10px;">
|
||||||
|
<button type="button" id="btn-test-connection" class="btn-secondary" style="padding: 8px 16px; border-radius: 6px; cursor: pointer;">Test Connection</button>
|
||||||
|
<span id="test-toast" class="toast hidden" style="margin-left: 10px;"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
@@ -9,6 +9,29 @@ const DEFAULT_SETTINGS = {
|
|||||||
urlType: "post"
|
urlType: "post"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeApiUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
url = url.trim().replace(/^['"]|['"]$/g, "");
|
||||||
|
if (!url) return "";
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
url = "http://" + url;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
let path = parsed.pathname.replace(/\/+$/, "");
|
||||||
|
if (!path || path === "") {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
} else if (path.endsWith("/api/v2")) {
|
||||||
|
return `${parsed.origin}/upload`;
|
||||||
|
} else if (!path.endsWith("/upload")) {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const form = document.getElementById("settings-form");
|
const form = document.getElementById("settings-form");
|
||||||
const modeDesktop = document.getElementById("mode-desktop");
|
const modeDesktop = document.getElementById("mode-desktop");
|
||||||
@@ -21,6 +44,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const isOc = document.getElementById("isOc");
|
const isOc = document.getElementById("isOc");
|
||||||
const urlType = document.getElementById("urlType");
|
const urlType = document.getElementById("urlType");
|
||||||
const saveToast = document.getElementById("save-toast");
|
const saveToast = document.getElementById("save-toast");
|
||||||
|
const btnTest = document.getElementById("btn-test-connection");
|
||||||
|
const testToast = document.getElementById("test-toast");
|
||||||
|
|
||||||
// Load existing settings
|
// Load existing settings
|
||||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => {
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => {
|
||||||
@@ -42,9 +67,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// Save settings on form submit
|
// Save settings on form submit
|
||||||
form.addEventListener("submit", (e) => {
|
form.addEventListener("submit", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const rawUrl = apiUrl.value.trim();
|
||||||
|
const normUrl = normalizeApiUrl(rawUrl);
|
||||||
|
if (normUrl && normUrl !== rawUrl) {
|
||||||
|
apiUrl.value = normUrl;
|
||||||
|
}
|
||||||
|
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
uploadMode: modeDirect.checked ? "direct" : "desktop",
|
uploadMode: modeDirect.checked ? "direct" : "desktop",
|
||||||
apiUrl: apiUrl.value.trim(),
|
apiUrl: normUrl || rawUrl,
|
||||||
apiKey: apiKey.value.trim(),
|
apiKey: apiKey.value.trim(),
|
||||||
rating: rating.value,
|
rating: rating.value,
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
@@ -60,4 +91,39 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}, 2500);
|
}, 2500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test Connection button
|
||||||
|
if (btnTest) {
|
||||||
|
btnTest.addEventListener("click", () => {
|
||||||
|
const url = apiUrl.value.trim();
|
||||||
|
const key = apiKey.value.trim();
|
||||||
|
if (!url || !key) {
|
||||||
|
if (testToast) {
|
||||||
|
testToast.textContent = "Please enter API URL and API Key.";
|
||||||
|
testToast.style.color = "#ff4444";
|
||||||
|
testToast.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnTest.disabled = true;
|
||||||
|
btnTest.textContent = "Testing...";
|
||||||
|
if (testToast) testToast.classList.add("hidden");
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ action: "test_connection", apiUrl: url, apiKey: key }, (res) => {
|
||||||
|
btnTest.disabled = false;
|
||||||
|
btnTest.textContent = "Test Connection";
|
||||||
|
if (testToast) {
|
||||||
|
testToast.classList.remove("hidden");
|
||||||
|
if (res && res.success) {
|
||||||
|
testToast.textContent = "✓ " + res.msg;
|
||||||
|
testToast.style.color = "#00e676";
|
||||||
|
} else {
|
||||||
|
testToast.textContent = "❌ " + (res ? res.msg : "Test failed");
|
||||||
|
testToast.style.color = "#ff4444";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,12 +20,79 @@ html, body {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
background: #191b22;
|
background: #191b22;
|
||||||
border-bottom: 1px solid #282a36;
|
border-bottom: 1px solid #282a36;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: #282c3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card {
|
||||||
|
background: #191b26;
|
||||||
|
border: 1px solid #0066ff44;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4da6ff;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card .field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #a0a6b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card input {
|
||||||
|
background: #12131a;
|
||||||
|
border: 1px solid #282c3f;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-toast {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<img src="../icons/icon32.png" alt="f0ckm Logo" class="brand-icon">
|
<img src="../icons/icon32.png" alt="f0ckm Logo" class="brand-icon">
|
||||||
<span class="brand-name">f0ckm Uploader</span>
|
<span class="brand-name">f0ckm Uploader</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="btn-open-options" class="icon-btn" title="Open Settings Options">⚙️</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mode-selector">
|
<div class="mode-selector">
|
||||||
@@ -26,6 +27,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<div id="api-config-card" class="api-config-card hidden">
|
||||||
|
<div class="config-header">
|
||||||
|
<span>⚙️ Direct API Credentials</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="popup-api-url">API URL:</label>
|
||||||
|
<input type="url" id="popup-api-url" placeholder="https://f0ckm.com/api/v2/upload">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="popup-api-key">API Key:</label>
|
||||||
|
<input type="password" id="popup-api-key" placeholder="Secret API key">
|
||||||
|
</div>
|
||||||
|
<div class="config-actions">
|
||||||
|
<button id="btn-save-api-config" class="btn btn-sm btn-primary">Save API Info</button>
|
||||||
|
<button id="btn-popup-test" class="btn btn-sm btn-accent">Test</button>
|
||||||
|
<span id="popup-api-toast" class="popup-toast"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="active-tab-card" class="active-tab-card">
|
<div id="active-tab-card" class="active-tab-card">
|
||||||
<div class="card-top">
|
<div class="card-top">
|
||||||
<span id="site-badge" class="site-badge">🌐 Web Page</span>
|
<span id="site-badge" class="site-badge">🌐 Web Page</span>
|
||||||
|
|||||||
@@ -1,6 +1,37 @@
|
|||||||
|
function normalizeApiUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
url = url.trim().replace(/^['"]|['"]$/g, "");
|
||||||
|
if (!url) return "";
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
url = "http://" + url;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
let path = parsed.pathname.replace(/\/+$/, "");
|
||||||
|
if (!path || path === "") {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
} else if (path.endsWith("/api/v2")) {
|
||||||
|
return `${parsed.origin}/upload`;
|
||||||
|
} else if (!path.endsWith("/upload")) {
|
||||||
|
return `${parsed.origin}/api/v2/upload`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initPopup() {
|
function initPopup() {
|
||||||
const modeDesktop = document.getElementById("mode-desktop");
|
const modeDesktop = document.getElementById("mode-desktop");
|
||||||
const modeDirect = document.getElementById("mode-direct");
|
const modeDirect = document.getElementById("mode-direct");
|
||||||
|
const btnOpenOptions = document.getElementById("btn-open-options");
|
||||||
|
const apiConfigCard = document.getElementById("api-config-card");
|
||||||
|
const popupApiUrl = document.getElementById("popup-api-url");
|
||||||
|
const popupApiKey = document.getElementById("popup-api-key");
|
||||||
|
const btnSaveApiConfig = document.getElementById("btn-save-api-config");
|
||||||
|
const btnPopupTest = document.getElementById("btn-popup-test");
|
||||||
|
const popupApiToast = document.getElementById("popup-api-toast");
|
||||||
|
|
||||||
const btnUploadTab = document.getElementById("btn-upload-tab");
|
const btnUploadTab = document.getElementById("btn-upload-tab");
|
||||||
const btnUploadText = document.getElementById("btn-upload-text");
|
const btnUploadText = document.getElementById("btn-upload-text");
|
||||||
const btnUploadUrl = document.getElementById("btn-upload-url");
|
const btnUploadUrl = document.getElementById("btn-upload-url");
|
||||||
@@ -17,26 +48,115 @@ function initPopup() {
|
|||||||
|
|
||||||
let activeTabUrl = "";
|
let activeTabUrl = "";
|
||||||
|
|
||||||
// 1. Load settings asynchronously without blocking UI initialization
|
if (btnOpenOptions) {
|
||||||
try {
|
btnOpenOptions.addEventListener("click", () => {
|
||||||
chrome.storage.sync.get({ uploadMode: "desktop" }, (settings) => {
|
if (chrome.runtime.openOptionsPage) {
|
||||||
if (chrome.runtime.lastError || !settings) return;
|
chrome.runtime.openOptionsPage();
|
||||||
if (settings.uploadMode === "direct") {
|
|
||||||
if (modeDirect) modeDirect.checked = true;
|
|
||||||
} else {
|
} else {
|
||||||
if (modeDesktop) modeDesktop.checked = true;
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModeUI(mode) {
|
||||||
|
if (mode === "direct") {
|
||||||
|
if (modeDirect) modeDirect.checked = true;
|
||||||
|
if (apiConfigCard) apiConfigCard.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
if (modeDesktop) modeDesktop.checked = true;
|
||||||
|
if (apiConfigCard) apiConfigCard.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Load settings & drafts asynchronously without blocking UI initialization
|
||||||
|
try {
|
||||||
|
chrome.storage.sync.get({ uploadMode: "desktop", apiUrl: "", apiKey: "", draftUrl: "" }, (settings) => {
|
||||||
|
if (chrome.runtime.lastError || !settings) return;
|
||||||
|
if (popupApiUrl) popupApiUrl.value = settings.apiUrl || "";
|
||||||
|
if (popupApiKey) popupApiKey.value = settings.apiKey || "";
|
||||||
|
if (urlInput && settings.draftUrl) urlInput.value = settings.draftUrl;
|
||||||
|
updateModeUI(settings.uploadMode);
|
||||||
|
});
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Auto-save on input so values are never lost if popup closes when copying/switching tabs
|
||||||
|
if (popupApiUrl) {
|
||||||
|
popupApiUrl.addEventListener("input", () => {
|
||||||
|
chrome.storage.sync.set({ apiUrl: popupApiUrl.value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (popupApiKey) {
|
||||||
|
popupApiKey.addEventListener("input", () => {
|
||||||
|
chrome.storage.sync.set({ apiKey: popupApiKey.value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (urlInput) {
|
||||||
|
urlInput.addEventListener("input", () => {
|
||||||
|
chrome.storage.sync.set({ draftUrl: urlInput.value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (modeDesktop) {
|
if (modeDesktop) {
|
||||||
modeDesktop.addEventListener("change", () => {
|
modeDesktop.addEventListener("change", () => {
|
||||||
chrome.storage.sync.set({ uploadMode: "desktop" });
|
chrome.storage.sync.set({ uploadMode: "desktop" }, () => {
|
||||||
|
updateModeUI("desktop");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (modeDirect) {
|
if (modeDirect) {
|
||||||
modeDirect.addEventListener("change", () => {
|
modeDirect.addEventListener("change", () => {
|
||||||
chrome.storage.sync.set({ uploadMode: "direct" });
|
chrome.storage.sync.set({ uploadMode: "direct" }, () => {
|
||||||
|
updateModeUI("direct");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnSaveApiConfig) {
|
||||||
|
btnSaveApiConfig.addEventListener("click", () => {
|
||||||
|
const rawUrl = popupApiUrl ? popupApiUrl.value.trim() : "";
|
||||||
|
const key = popupApiKey ? popupApiKey.value.trim() : "";
|
||||||
|
const normUrl = normalizeApiUrl(rawUrl);
|
||||||
|
if (popupApiUrl && normUrl) popupApiUrl.value = normUrl;
|
||||||
|
|
||||||
|
chrome.storage.sync.set({ apiUrl: normUrl || rawUrl, apiKey: key }, () => {
|
||||||
|
if (popupApiToast) {
|
||||||
|
popupApiToast.textContent = "Saved!";
|
||||||
|
popupApiToast.style.color = "#00e676";
|
||||||
|
setTimeout(() => { popupApiToast.textContent = ""; }, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnPopupTest) {
|
||||||
|
btnPopupTest.addEventListener("click", () => {
|
||||||
|
const url = popupApiUrl ? popupApiUrl.value.trim() : "";
|
||||||
|
const key = popupApiKey ? popupApiKey.value.trim() : "";
|
||||||
|
if (!url || !key) {
|
||||||
|
if (popupApiToast) {
|
||||||
|
popupApiToast.textContent = "Enter URL & Key!";
|
||||||
|
popupApiToast.style.color = "#ff4444";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btnPopupTest.disabled = true;
|
||||||
|
if (popupApiToast) {
|
||||||
|
popupApiToast.textContent = "Testing...";
|
||||||
|
popupApiToast.style.color = "#4da6ff";
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ action: "test_connection", apiUrl: url, apiKey: key }, (res) => {
|
||||||
|
btnPopupTest.disabled = false;
|
||||||
|
if (popupApiToast) {
|
||||||
|
if (res && res.success) {
|
||||||
|
popupApiToast.textContent = "✓ OK!";
|
||||||
|
popupApiToast.style.color = "#00e676";
|
||||||
|
} else {
|
||||||
|
popupApiToast.textContent = "❌ " + (res ? res.msg : "Failed");
|
||||||
|
popupApiToast.style.color = "#ff4444";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +290,7 @@ function initPopup() {
|
|||||||
if (response && response.resultUrl) {
|
if (response && response.resultUrl) {
|
||||||
showSuccessResult(response.resultUrl);
|
showSuccessResult(response.resultUrl);
|
||||||
} else {
|
} else {
|
||||||
showStatus("URL Upload queued! Link copied to clipboard.", false);
|
showStatus("Upload failed or queued. Check notification / settings.", false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user