add android app

This commit is contained in:
2026-08-12 22:09:29 +02:00
parent 43e95c9d3e
commit 7e723ae821
30 changed files with 1872 additions and 45 deletions

1
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build/

View 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")
}

View 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>

View 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()
}
)
}
}
}

View File

@@ -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()
}
}

View 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)
}
}

View 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
}
}

View 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>

View 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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View 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>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">fuggloader</string>
</resources>

View 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>