add android app
This commit is contained in:
1
android/app/.gitignore
vendored
Normal file
1
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build/
|
||||
56
android/app/build.gradle.kts
Normal file
56
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,56 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fuggloader.app"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.fuggloader.app"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
dataBinding = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
|
||||
// OkHttp for fast streaming uploads
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
|
||||
// Coroutines for background tasks
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
}
|
||||
63
android/app/src/main/AndroidManifest.xml
Normal file
63
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FuggloaderApp"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<!-- Main Configuration Activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.FuggloaderApp">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Share Target Activity (Receives Android Share Intent) -->
|
||||
<activity
|
||||
android:name=".ShareActivity"
|
||||
android:exported="true"
|
||||
android:label="Upload to fuggloader"
|
||||
android:theme="@style/Theme.FuggloaderApp.Translucent">
|
||||
|
||||
<!-- Single File / URL Share Intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Text / URL Share Intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Multiple Files Share Intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
114
android/app/src/main/java/com/fuggloader/app/MainActivity.kt
Normal file
114
android/app/src/main/java/com/fuggloader/app/MainActivity.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package com.fuggloader.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.fuggloader.app.databinding.ActivityMainBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var prefs: PreferencesManager
|
||||
private lateinit var uploader: UploaderService
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
prefs = PreferencesManager(this)
|
||||
uploader = UploaderService(prefs)
|
||||
|
||||
setupRatingSpinner()
|
||||
setupVisibilitySpinner()
|
||||
loadSettings()
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
saveSettings()
|
||||
Toast.makeText(this, "Settings saved successfully!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
binding.btnTestConnection.setOnClickListener {
|
||||
saveSettings()
|
||||
testConnection()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRatingSpinner() {
|
||||
val ratings = arrayOf("None", "SFW (sfw)", "NSFW (nsfw)", "NSFL (nsfl)")
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, ratings)
|
||||
binding.spinnerRating.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupVisibilitySpinner() {
|
||||
val visibilities = arrayOf("Public (0)", "Unlisted (1)", "Private (2)")
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, visibilities)
|
||||
binding.spinnerVisibility.adapter = adapter
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
binding.etApiUrl.setText(prefs.apiUrl)
|
||||
binding.etApiKey.setText(prefs.apiKey)
|
||||
binding.etTags.setText(prefs.defaultTags)
|
||||
|
||||
when (prefs.defaultRating) {
|
||||
"sfw" -> binding.spinnerRating.setSelection(1)
|
||||
"nsfw" -> binding.spinnerRating.setSelection(2)
|
||||
"nsfl" -> binding.spinnerRating.setSelection(3)
|
||||
else -> binding.spinnerRating.setSelection(0)
|
||||
}
|
||||
|
||||
when (prefs.defaultVisibility) {
|
||||
"1" -> binding.spinnerVisibility.setSelection(1)
|
||||
"2" -> binding.spinnerVisibility.setSelection(2)
|
||||
else -> binding.spinnerVisibility.setSelection(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
prefs.apiUrl = binding.etApiUrl.text.toString().trim()
|
||||
prefs.apiKey = binding.etApiKey.text.toString().trim()
|
||||
prefs.defaultTags = binding.etTags.text.toString().trim()
|
||||
|
||||
prefs.defaultRating = when (binding.spinnerRating.selectedItemPosition) {
|
||||
1 -> "sfw"
|
||||
2 -> "nsfw"
|
||||
3 -> "nsfl"
|
||||
else -> "none"
|
||||
}
|
||||
|
||||
prefs.defaultVisibility = binding.spinnerVisibility.selectedItemPosition.toString()
|
||||
}
|
||||
|
||||
private fun testConnection() {
|
||||
if (!prefs.isConfigured()) {
|
||||
Toast.makeText(this, "Please enter both API URL and API Key", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnTestConnection.isEnabled = false
|
||||
binding.btnTestConnection.text = "Testing..."
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val result = uploader.testConnection()
|
||||
binding.btnTestConnection.isEnabled = true
|
||||
binding.btnTestConnection.text = "Test"
|
||||
|
||||
result.fold(
|
||||
onSuccess = { msg ->
|
||||
Toast.makeText(this@MainActivity, "Connection successful!\n$msg", Toast.LENGTH_LONG).show()
|
||||
},
|
||||
onFailure = { error ->
|
||||
Log.e("MainActivity", "Test connection failed", error)
|
||||
Toast.makeText(this@MainActivity, "Connection failed: ${error.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.fuggloader.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class PreferencesManager(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("fuggloader_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
var apiUrl: String
|
||||
get() = prefs.getString("api_url", "") ?: ""
|
||||
set(value) = prefs.edit().putString("api_url", value).apply()
|
||||
|
||||
var apiKey: String
|
||||
get() = prefs.getString("api_key", "") ?: ""
|
||||
set(value) = prefs.edit().putString("api_key", value).apply()
|
||||
|
||||
var defaultRating: String
|
||||
get() = prefs.getString("default_rating", "none") ?: "none"
|
||||
set(value) = prefs.edit().putString("default_rating", value).apply()
|
||||
|
||||
var defaultTags: String
|
||||
get() = prefs.getString("default_tags", "android,upload") ?: "android,upload"
|
||||
set(value) = prefs.edit().putString("default_tags", value).apply()
|
||||
|
||||
var defaultVisibility: String
|
||||
get() = prefs.getString("default_visibility", "0") ?: "0"
|
||||
set(value) = prefs.edit().putString("default_visibility", value).apply()
|
||||
|
||||
fun isConfigured(): Boolean {
|
||||
return apiUrl.isNotBlank() && apiKey.isNotBlank()
|
||||
}
|
||||
}
|
||||
184
android/app/src/main/java/com/fuggloader/app/ShareActivity.kt
Normal file
184
android/app/src/main/java/com/fuggloader/app/ShareActivity.kt
Normal file
@@ -0,0 +1,184 @@
|
||||
package com.fuggloader.app
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.fuggloader.app.databinding.ActivityShareBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ShareActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityShareBinding
|
||||
private lateinit var prefs: PreferencesManager
|
||||
private lateinit var uploader: UploaderService
|
||||
|
||||
private var uploadedUrl: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityShareBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
prefs = PreferencesManager(this)
|
||||
uploader = UploaderService(prefs)
|
||||
|
||||
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)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnClose.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
handleShareIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleShareIntent(intent: Intent) {
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
|
||||
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(targetUrl)
|
||||
} else {
|
||||
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
if (streamUri != null) {
|
||||
processFileUpload(streamUri)
|
||||
} else {
|
||||
showError("No valid file or URL found in share intent")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val streamUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
if (streamUri != null) {
|
||||
processFileUpload(streamUri)
|
||||
} else {
|
||||
showError("No stream URI found for file share")
|
||||
}
|
||||
}
|
||||
} 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(uris[0])
|
||||
} else {
|
||||
showError("No files found in multi-share intent")
|
||||
}
|
||||
} else {
|
||||
showError("Unsupported share action: $action")
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.isNull_or_blank_url(): Boolean {
|
||||
if (this.isNullOrBlank()) return true
|
||||
val trimmed = this.trim()
|
||||
return !trimmed.startsWith("http://") && !trimmed.startsWith("https://")
|
||||
}
|
||||
|
||||
private fun extractUrl(text: String): String {
|
||||
val trimmed = text.trim()
|
||||
val parts = trimmed.split("\\s+".toRegex())
|
||||
for (part in parts) {
|
||||
if (part.startsWith("http://") || part.startsWith("https://")) {
|
||||
return part
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
private fun processFileUpload(uri: Uri) {
|
||||
binding.tvStatus.text = "Uploading file..."
|
||||
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,
|
||||
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(url: String) {
|
||||
binding.tvStatus.text = "Uploading URL..."
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.progressBar.isIndeterminate = true
|
||||
binding.tvPercent.text = ""
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val result = uploader.uploadUrl(url)
|
||||
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)
|
||||
}
|
||||
}
|
||||
223
android/app/src/main/java/com/fuggloader/app/UploaderService.kt
Normal file
223
android/app/src/main/java/com/fuggloader/app/UploaderService.kt
Normal file
@@ -0,0 +1,223 @@
|
||||
package com.fuggloader.app
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okio.BufferedSink
|
||||
import okio.source
|
||||
import org.json.JSONObject
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UploaderService(private val prefs: PreferencesManager) {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.MINUTES)
|
||||
.readTimeout(30, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
suspend fun uploadFile(
|
||||
contentResolver: ContentResolver,
|
||||
uri: Uri,
|
||||
rating: String = prefs.defaultRating,
|
||||
tags: String = prefs.defaultTags,
|
||||
visibility: String = prefs.defaultVisibility,
|
||||
onProgress: (percent: Int) -> Unit
|
||||
): Result<String> = withContext(Dispatchers.IO) {
|
||||
Log.d("UploaderService", "Starting uploadFile for $uri")
|
||||
try {
|
||||
val fileName = getFileName(contentResolver, uri) ?: "upload_${System.currentTimeMillis()}"
|
||||
val mimeType = contentResolver.getType(uri) ?: "application/octet-stream"
|
||||
val fileSize = getFileSize(contentResolver, uri)
|
||||
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
?: return@withContext Result.failure(Exception("Cannot open file stream"))
|
||||
|
||||
val requestBody = object : RequestBody() {
|
||||
override fun contentType() = mimeType.toMediaTypeOrNull()
|
||||
|
||||
override fun contentLength() = fileSize
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val source = inputStream.source()
|
||||
val buffer = okio.Buffer()
|
||||
var totalBytesRead = 0L
|
||||
var readCount: Long
|
||||
|
||||
while (source.read(buffer, 8192L).also { readCount = it } != -1L) {
|
||||
sink.write(buffer, readCount)
|
||||
totalBytesRead += readCount
|
||||
if (fileSize > 0) {
|
||||
val percent = ((totalBytesRead.toDouble() / fileSize.toDouble()) * 100).toInt()
|
||||
onProgress(percent.coerceIn(0, 99))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val multipartBuilder = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", fileName, requestBody)
|
||||
|
||||
if (rating != "none") {
|
||||
multipartBuilder.addFormDataPart("rating", rating)
|
||||
}
|
||||
|
||||
multipartBuilder.addFormDataPart("tags", tags)
|
||||
.addFormDataPart("visibility", visibility)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(prefs.apiUrl)
|
||||
.addHeader("X-Api-Key", prefs.apiKey)
|
||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||
.post(multipartBuilder.build())
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (response.isSuccessful) {
|
||||
onProgress(100)
|
||||
val postUrl = parsePostUrl(responseBody)
|
||||
Result.success(postUrl)
|
||||
} else {
|
||||
Log.e("UploaderService", "Upload failed: HTTP ${response.code}, Body: $responseBody")
|
||||
Result.failure(Exception("HTTP ${response.code}: $responseBody"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadUrl(
|
||||
url: String,
|
||||
rating: String = prefs.defaultRating,
|
||||
tags: String = prefs.defaultTags,
|
||||
visibility: String = prefs.defaultVisibility
|
||||
): Result<String> = withContext(Dispatchers.IO) {
|
||||
Log.d("UploaderService", "Starting uploadUrl for $url")
|
||||
try {
|
||||
val json = JSONObject().apply {
|
||||
put("url", url)
|
||||
if (rating != "none") {
|
||||
put("rating", rating)
|
||||
}
|
||||
put("tags", tags)
|
||||
put("visibility", visibility)
|
||||
}
|
||||
|
||||
val requestBody = RequestBody.create(
|
||||
"application/json; charset=utf-8".toMediaTypeOrNull(),
|
||||
json.toString()
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(prefs.apiUrl)
|
||||
.addHeader("X-Api-Key", prefs.apiKey)
|
||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val postUrl = parsePostUrl(responseBody)
|
||||
Result.success(postUrl)
|
||||
} else {
|
||||
Log.e("UploaderService", "URL Upload failed: HTTP ${response.code}, Body: $responseBody")
|
||||
Result.failure(Exception("HTTP ${response.code}: $responseBody"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testConnection(): Result<String> = withContext(Dispatchers.IO) {
|
||||
Log.d("UploaderService", "Starting testConnection ping")
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(prefs.apiUrl)
|
||||
.addHeader("X-Api-Key", prefs.apiKey)
|
||||
.addHeader("User-Agent", "fuggloader-android/1.0")
|
||||
.post(ByteArray(0).toRequestBody(null))
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
when (response.code) {
|
||||
200 -> Result.success("Success!")
|
||||
401 -> Result.failure(Exception("Unauthorized: Invalid API Key"))
|
||||
400, 422 -> Result.success("Success! (Auth verified)")
|
||||
else -> Result.failure(Exception("HTTP ${response.code}: $responseBody"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parsePostUrl(jsonResponse: String): String {
|
||||
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")
|
||||
} else {
|
||||
jsonResponse
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
jsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileName(contentResolver: ContentResolver, 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('/')
|
||||
if (cut != null && cut != -1) {
|
||||
result = result?.substring(cut + 1)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getFileSize(contentResolver: ContentResolver, uri: Uri): Long {
|
||||
var size: Long = -1
|
||||
if (uri.scheme == "content") {
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val index = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
if (index != -1) {
|
||||
size = cursor.getLong(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
157
android/app/src/main/res/layout/activity_main.xml
Normal file
157
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#121216"
|
||||
android:fillViewport="true"
|
||||
android:padding="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="fuggloader"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Configure your server credentials to enable Android Share menu uploads."
|
||||
android:textColor="#A0A0B0"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- API Endpoint URL -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="API Endpoint URL"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="#1E1E24"
|
||||
android:hint="https://your-fuggloader-site.com/api/v2/upload"
|
||||
android:inputType="textUri"
|
||||
android:padding="12dp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textColorHint="#606070" />
|
||||
|
||||
<!-- API Key -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="API Key"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="#1E1E24"
|
||||
android:hint="Paste your API key here"
|
||||
android:inputType="textPassword"
|
||||
android:padding="12dp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textColorHint="#606070" />
|
||||
|
||||
<!-- Default Tags -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Default Tags (comma separated)"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etTags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="#1E1E24"
|
||||
android:hint="android, mobile"
|
||||
android:inputType="text"
|
||||
android:padding="12dp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textColorHint="#606070" />
|
||||
|
||||
<!-- Default Rating -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Default Rating"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="#1E1E24"
|
||||
android:padding="8dp" />
|
||||
|
||||
<!-- Default Visibility -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Default Visibility"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerVisibility"
|
||||
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"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnTestConnection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="#2E2E3A"
|
||||
android:text="Test"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="#6200EE"
|
||||
android:text="Save Settings"
|
||||
android:textColor="#FFFFFF" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
101
android/app/src/main/res/layout/activity_share.xml
Normal file
101
android/app/src/main/res/layout/activity_share.xml
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#90000000"
|
||||
android:padding="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="#1E1E24"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="fuggloader Upload"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Preparing upload..."
|
||||
android:textColor="#B0B0C0"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:max="100" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPercent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="0%"
|
||||
android:textColor="#808090"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResultUrl"
|
||||
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:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutButtons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCopy"
|
||||
android:layout_width="0dp"
|
||||
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
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="12dp"
|
||||
android:backgroundTint="#333333"
|
||||
android:text="Close"
|
||||
android:textColor="#CCCCCC"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
10
android/app/src/main/res/values/colors.xml
Normal file
10
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_500">#6200EE</color>
|
||||
<color name="purple_700">#3700B3</color>
|
||||
<color name="teal_200">#03DAC5</color>
|
||||
<color name="teal_700">#018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
3
android/app/src/main/res/values/strings.xml
Normal file
3
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">fuggloader</string>
|
||||
</resources>
|
||||
19
android/app/src/main/res/values/themes.xml
Normal file
19
android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme -->
|
||||
<style name="Theme.FuggloaderApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="android:statusBarColor">#121216</item>
|
||||
</style>
|
||||
|
||||
<!-- Translucent Overlay Theme for Share Activity -->
|
||||
<style name="Theme.FuggloaderApp.Translucent" parent="Theme.FuggloaderApp">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user