diff --git a/README.md b/README.md index e15b59f..80aeb98 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ Shows a preview with custom options and copies the URL to the clipboard after up +> [!WARNING] +> Google Play is currently in Closed Testing. +> To be included see [this discussion](https://github.com/cssnr/zipline-android/discussions/25). + +[![Google Play](https://raw.githubusercontent.com/smashedr/repo-images/refs/heads/master/google/get-on-play-400.webp)](https://play.google.com/store/apps/details?id=org.cssnr.zipline) + _Note: Until published on the play store, you may need to allow installation of apps from unknown sources._ Downloading and Installing the [apk](https://github.com/cssnr/zipline-android/releases/latest/download/app-release.apk) @@ -85,6 +91,7 @@ Additionally, the URL is copied to the clipboard and the preview is show in the - Share or Open any file or URL to your Zipline server. - Single file previews most media with custom name option. - Multiple file upload previews, options and file selector. +- Widget with stats, custom update interval, upload button. ### Planned @@ -203,13 +210,13 @@ For more details, see the [ADB Documentation](https://developer.android.com/tool ## Google Services This app uses Firebase Google Services. Building requires a valid `google-services.json` file in the `app` directory. -You must add `org.cssnr.zipline.dev` to a Firebase campaign here: https://firebase.google.com/ +You must add `org.cssnr.zipline` to a Firebase campaign here: https://firebase.google.com/ To enable/disable Firebase DebugView use the following commands: ```shell # set -adb shell setprop debug.firebase.analytics.app org.cssnr.zipline.dev +adb shell setprop debug.firebase.analytics.app org.cssnr.zipline # unset adb shell setprop debug.firebase.analytics.app .none. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37c0200..eeb1291 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - //alias(libs.plugins.ksp) + alias(libs.plugins.ksp) alias(libs.plugins.google.services) alias(libs.plugins.firebase.crashlytics) } @@ -62,6 +62,9 @@ dependencies { implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.work.runtime.ktx) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.runtime) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) @@ -74,6 +77,7 @@ dependencies { implementation(libs.media3.ui) implementation(libs.media3.ui.compose) implementation(libs.taptargetview) + ksp(libs.androidx.room.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bd9b891..e4fa445 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,7 +20,8 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile -# Logging + +## Logging -assumenosideeffects class android.util.Log { public static int d(...); public static int v(...); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 547fd0d..01e8aff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,23 @@ android:name="firebase_crashlytics_collection_enabled" android:value="${firebaseCrashlyticsEnabled}" /> + + + + + + + + + + + + + diff --git a/app/src/main/java/org/cssnr/zipline/MainActivity.kt b/app/src/main/java/org/cssnr/zipline/MainActivity.kt index b5acace..c94b325 100644 --- a/app/src/main/java/org/cssnr/zipline/MainActivity.kt +++ b/app/src/main/java/org/cssnr/zipline/MainActivity.kt @@ -1,8 +1,10 @@ package org.cssnr.zipline import android.annotation.SuppressLint +import android.appwidget.AppWidgetManager import android.content.ClipData import android.content.ClipboardManager +import android.content.ComponentName import android.content.Context import android.content.Context.CLIPBOARD_SERVICE import android.content.Intent @@ -24,8 +26,15 @@ import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.preference.PreferenceManager +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import org.cssnr.zipline.databinding.ActivityMainBinding +import org.cssnr.zipline.widget.WidgetProvider +import org.cssnr.zipline.work.APP_WORKER_CONSTRAINTS +import org.cssnr.zipline.work.AppWorker import java.net.URL +import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { @@ -60,6 +69,27 @@ class MainActivity : AppCompatActivity() { // Set Default Preferences PreferenceManager.setDefaultValues(this, R.xml.preferences, false) + PreferenceManager.setDefaultValues(this, R.xml.preferences_widget, false) + + // TODO: Improve initialization of the WorkRequest + val workInterval = preferences.getString("work_interval", null) ?: "0" + Log.i("Main[onCreate]", "workInterval: $workInterval") + if (workInterval != "0") { + val workRequest = + PeriodicWorkRequestBuilder(workInterval.toLong(), TimeUnit.MINUTES) + .setConstraints(APP_WORKER_CONSTRAINTS) + .build() + Log.i("Main[onCreate]", "workRequest: $workRequest") + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "app_worker", + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } else { + // TODO: Confirm this is necessary... + Log.i("Main[onCreate]", "Ensuring Work is Disabled") + WorkManager.getInstance(this).cancelUniqueWork("app_worker") + } // Handle Custom Navigation Items binding.navView.setNavigationItemSelectedListener { menuItem -> @@ -102,12 +132,13 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Log.d("onNewIntent", "intent.data: ${intent.data}") + val action = intent.action + Log.d("onNewIntent", "action: $action") + Log.d("onNewIntent", "data: ${intent.data}") Log.d("onNewIntent", "intent.type: ${intent.type}") - Log.d("onNewIntent", "intent.action: ${intent.action}") val extraText = intent.getStringExtra(Intent.EXTRA_TEXT) - Log.d("onNewIntent", "extraText: $extraText") + Log.d("onNewIntent", "extraText: ${extraText?.take(100)}") val savedUrl = preferences.getString("ziplineUrl", null) val authToken = preferences.getString("ziplineToken", null) @@ -123,7 +154,7 @@ class MainActivity : AppCompatActivity() { .build() ) - } else if (Intent.ACTION_MAIN == intent.action) { + } else if (Intent.ACTION_MAIN == action) { Log.d("onNewIntent", "ACTION_MAIN") binding.drawerLayout.closeDrawers() @@ -157,7 +188,7 @@ class MainActivity : AppCompatActivity() { filePickerLauncher.launch(arrayOf("*/*")) } - } else if (Intent.ACTION_SEND == intent.action) { + } else if (Intent.ACTION_SEND == action) { Log.d("onNewIntent", "ACTION_SEND") val fileUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -169,7 +200,7 @@ class MainActivity : AppCompatActivity() { Log.d("onNewIntent", "File URI: $fileUri") if (fileUri == null && !extraText.isNullOrEmpty()) { - Log.d("onNewIntent", "SEND TEXT DETECTED: $extraText") + Log.d("onNewIntent", "SEND TEXT DETECTED: ${extraText.take(100)}") //if (extraText.lowercase().startsWith("http")) { //if (Patterns.WEB_URL.matcher(extraText).matches()) { if (isURL(extraText)) { @@ -204,7 +235,7 @@ class MainActivity : AppCompatActivity() { showPreview(fileUri) } - } else if (Intent.ACTION_SEND_MULTIPLE == intent.action) { + } else if (Intent.ACTION_SEND_MULTIPLE == action) { Log.d("onNewIntent", "ACTION_SEND_MULTIPLE") val fileUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -221,18 +252,33 @@ class MainActivity : AppCompatActivity() { } showMultiPreview(fileUris) - } else if (Intent.ACTION_VIEW == intent.action) { + } else if (Intent.ACTION_VIEW == action) { Log.d("onNewIntent", "ACTION_VIEW") Log.d("onNewIntent", "File URI: ${intent.data}") showPreview(intent.data) + } else if ("UPLOAD_FILE" == action) { + Log.d("handleIntent", "UPLOAD_FILE") + + filePickerLauncher.launch(arrayOf("*/*")) + } else { - Toast.makeText(this, "That's a Bug!", Toast.LENGTH_SHORT).show() - Log.w("onNewIntent", "BUG: UNKNOWN intent.action: ${intent.action}") + Toast.makeText(this, "That's a Bug!", Toast.LENGTH_LONG).show() + Log.w("onNewIntent", "UNKNOWN INTENT - action: $action") } } + override fun onStop() { + super.onStop() + Log.d("Main[onStop]", "MainActivity - onStop") + // Update Widget + val appWidgetManager = AppWidgetManager.getInstance(this) + val componentName = ComponentName(this, WidgetProvider::class.java) + val ids = appWidgetManager.getAppWidgetIds(componentName) + WidgetProvider().onUpdate(this, appWidgetManager, ids) + } + private fun isURL(url: String): Boolean { return try { URL(url) diff --git a/app/src/main/java/org/cssnr/zipline/api/ZiplineApi.kt b/app/src/main/java/org/cssnr/zipline/api/ZiplineApi.kt index 13fbea6..afd048d 100644 --- a/app/src/main/java/org/cssnr/zipline/api/ZiplineApi.kt +++ b/app/src/main/java/org/cssnr/zipline/api/ZiplineApi.kt @@ -1,11 +1,11 @@ package org.cssnr.zipline.api import android.content.Context -import android.content.SharedPreferences import android.util.Log import android.webkit.CookieManager import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.google.gson.annotations.SerializedName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Cookie @@ -17,6 +17,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody +import org.cssnr.zipline.R import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -34,8 +35,7 @@ class ZiplineApi(private val context: Context, url: String? = null) { private var ziplineUrl: String private var ziplineToken: String - private val preferences: SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context) + private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(context) } private lateinit var cookieJar: SimpleCookieJar private lateinit var client: OkHttpClient @@ -43,24 +43,24 @@ class ZiplineApi(private val context: Context, url: String? = null) { init { ziplineUrl = url ?: preferences.getString("ziplineUrl", null) ?: "" ziplineToken = preferences.getString("ziplineToken", null) ?: "" - Log.d("ServerApi", "ziplineUrl: $ziplineUrl") - Log.d("ServerApi", "ziplineToken: $ziplineToken") + Log.d("ServerApi[init]", "ziplineUrl: $ziplineUrl") + Log.d("ServerApi[init]", "ziplineToken: $ziplineToken") api = createRetrofit().create(ApiService::class.java) } suspend fun login(host: String, user: String, pass: String): String? { - Log.d("login", "host: $host") - Log.d("login", "user/pass: ${user}/${pass}") + Log.d("Api[login]", "host: $host") + Log.d("Api[login]", "user/pass: ${user}/${pass}") return try { val loginResponse = api.postLogin(LoginRequest(user, pass)) - Log.i("login", "loginResponse.code(): ${loginResponse.code()}") + Log.i("Api[login]", "loginResponse.code(): ${loginResponse.code()}") if (loginResponse.isSuccessful) { val tokenResponse = api.getToken() val cookies = cookieJar.loadForRequest(host.toHttpUrl()) val cookieManager = CookieManager.getInstance() for (cookie in cookies) { - Log.d("login", "setCookie: $cookie") + Log.d("Api[login]", "setCookie: $cookie") //cookieManager.setCookie(host, cookie.toString()) cookieManager.setCookie(host, cookie.toString()) { cookieManager.flush() @@ -69,46 +69,56 @@ class ZiplineApi(private val context: Context, url: String? = null) { tokenResponse.token } else { //loginResponse.errorBody()?.string()?.take(200)?.let { - // Log.i("login", "errorBody: $it") + // Log.i("Api[login]", "errorBody: $it") //} - Log.i("login", "errorBody: ${loginResponse.errorBody()?.string()?.take(255)}") + Log.i("Api[login]", "errorBody: ${loginResponse.errorBody()?.string()?.take(255)}") null } } catch (e: Exception) { - Log.e("login", "Exception: ${e.message}") + Log.e("Api[login]", "Exception: ${e.message}") null } } suspend fun shorten(url: String, vanity: String?): Response { - Log.d("upload", "url: $url") - Log.d("upload", "vanity: $vanity") + Log.d("Api[upload]", "url: $url") + Log.d("Api[upload]", "vanity: $vanity") - val response = api.postShort(ziplineToken, ShortRequest(url, vanity, true)) + val response = api.postShort(ShortRequest(url, vanity, true)) if (response.code() == 401) { val token = reAuthenticate(api, ziplineUrl) - Log.d("Api[upload]", "token: $token") + Log.d("Api[upload]", "reAuthenticate: token: $token") if (token != null) { - return api.postShort(token, ShortRequest(url, vanity, true)) + return api.postShort(ShortRequest(url, vanity, true)) } } return response } - suspend fun upload( - fileName: String, - inputStream: InputStream, - ): Response { - Log.d("upload", "fileName: $fileName") + suspend fun upload(fileName: String, inputStream: InputStream): Response { + Log.d("Api[upload]", "fileName: $fileName") val fileNameFormat = preferences.getString("file_name_format", null) ?: "random" - Log.d("upload", "fileNameFormat: $fileNameFormat") + Log.d("Api[upload]", "fileNameFormat: $fileNameFormat") val multiPart: MultipartBody.Part = inputStreamToMultipart(inputStream, fileName) - val response = api.postUpload(ziplineToken, fileNameFormat.toString(), multiPart) + val response = api.postUpload(fileNameFormat.toString(), multiPart) + if (response.code() == 401) { + val token = reAuthenticate(api, ziplineUrl) + Log.d("Api[upload]", "reAuthenticate: token: $token") + if (token != null) { + return api.postUpload(fileNameFormat, multiPart) + } + } + return response + } + + suspend fun stats(): Response { + Log.d("Api[stats]", "stats") + val response = api.getStats() if (response.code() == 401) { val token = reAuthenticate(api, ziplineUrl) - Log.d("Api[upload]", "token: $token") + Log.d("Api[upload]", "reAuthenticate: token: $token") if (token != null) { - return api.postUpload(token, fileNameFormat, multiPart) + return api.getStats() } } return response @@ -137,7 +147,7 @@ class ZiplineApi(private val context: Context, url: String? = null) { private suspend fun inputStreamToMultipart( file: InputStream, - fileName: String + fileName: String, ): MultipartBody.Part { val contentType = URLConnection.guessContentTypeFromName(fileName) ?: "application/octet-stream" @@ -150,9 +160,19 @@ class ZiplineApi(private val context: Context, url: String? = null) { private fun createRetrofit(): Retrofit { val baseUrl = "${ziplineUrl}/api/" Log.d("createRetrofit", "baseUrl: $baseUrl") + val versionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName + val userAgent = "${context.getString(R.string.app_name)}/${versionName}" + Log.d("createRetrofit", "versionName: $versionName") cookieJar = SimpleCookieJar() client = OkHttpClient.Builder() .cookieJar(cookieJar) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .header("User-Agent", userAgent) + .header("authorization", ziplineToken) + .build() + chain.proceed(request) + } .build() return Retrofit.Builder() .baseUrl(baseUrl) @@ -168,17 +188,18 @@ class ZiplineApi(private val context: Context, url: String? = null) { @GET("user/token") suspend fun getToken(): TokenResponse + @GET("user/stats") + suspend fun getStats(): Response + @Multipart @POST("upload") suspend fun postUpload( - @Header("authorization") token: String, @Header("x-zipline-format") format: String, @Part file: MultipartBody.Part, ): Response @POST("user/urls") suspend fun postShort( - @Header("authorization") token: String, @Body request: ShortRequest, ): Response } @@ -222,6 +243,18 @@ class ZiplineApi(private val context: Context, url: String? = null) { val url: String ) + data class StatsResponse( + @SerializedName("filesUploaded") val filesUploaded: Int, + @SerializedName("favoriteFiles") val favoriteFiles: Int, + @SerializedName("views") val views: Int, + @SerializedName("avgViews") val avgViews: Double, + @SerializedName("storageUsed") val storageUsed: Long, + @SerializedName("avgStorageUsed") val avgStorageUsed: Double, + @SerializedName("urlsCreated") val urlsCreated: Int, + @SerializedName("urlViews") val urlViews: Int, + ) + + inner class SimpleCookieJar : CookieJar { private val cookieStore = mutableMapOf>() @@ -244,6 +277,7 @@ class ZiplineApi(private val context: Context, url: String? = null) { } } + //data class LoginResponse( // val user: TokenUser //) diff --git a/app/src/main/java/org/cssnr/zipline/db/ServerDao.kt b/app/src/main/java/org/cssnr/zipline/db/ServerDao.kt new file mode 100644 index 0000000..1647b31 --- /dev/null +++ b/app/src/main/java/org/cssnr/zipline/db/ServerDao.kt @@ -0,0 +1,71 @@ +package org.cssnr.zipline.db + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Upsert + +@Dao +interface ServerDao { + @Upsert + fun upsert(stats: ServerEntity) + + @Query("SELECT * FROM servers WHERE url = :url LIMIT 1") + fun get(url: String): ServerEntity? + + @Query("SELECT * FROM servers ORDER BY ROWID") + fun getAll(): List + + @Query("UPDATE servers SET token = :token WHERE url = :url") + fun setToken(url: String, token: String) + + @Query("UPDATE servers SET active = 1 WHERE url = :url") + fun activate(url: String) + + @Delete + fun delete(server: ServerEntity) +} + + +@Entity(tableName = "servers") +data class ServerEntity( + @PrimaryKey val url: String, + val token: String = "", + val active: Boolean = false, + val filesUploaded: Int? = null, + val favoriteFiles: Int? = null, + val views: Int? = null, + val avgViews: Double? = null, + val storageUsed: Long? = null, + val avgStorageUsed: Double? = null, + val urlsCreated: Int? = null, + val urlViews: Int, +) + + +@Database(entities = [ServerEntity::class], version = 1) +abstract class ServerDatabase : RoomDatabase() { + abstract fun serverDao(): ServerDao + + companion object { + @Volatile + private var instance: ServerDatabase? = null + + fun getInstance(context: Context): ServerDatabase = + instance ?: synchronized(this) { + instance ?: Room.databaseBuilder( + context.applicationContext, + ServerDatabase::class.java, + "server-database" + ) + .fallbackToDestructiveMigration(true) + .build().also { instance = it } + } + } +} diff --git a/app/src/main/java/org/cssnr/zipline/ui/settings/SettingsFragment.kt b/app/src/main/java/org/cssnr/zipline/ui/settings/SettingsFragment.kt index 9cf0b82..a08aab5 100644 --- a/app/src/main/java/org/cssnr/zipline/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/org/cssnr/zipline/ui/settings/SettingsFragment.kt @@ -16,10 +16,14 @@ import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.firebase.analytics.ktx.analytics @@ -29,6 +33,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.cssnr.zipline.R import org.cssnr.zipline.api.FeedbackApi +import org.cssnr.zipline.work.APP_WORKER_CONSTRAINTS +import org.cssnr.zipline.work.AppWorker +import java.util.concurrent.TimeUnit class SettingsFragment : PreferenceFragmentCompat() { @@ -58,6 +65,21 @@ class SettingsFragment : PreferenceFragmentCompat() { val launcherAction = findPreference("launcher_action") launcherAction?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + // Widget Settings + findPreference("open_widget_settings")?.setOnPreferenceClickListener { + Log.d("open_widget_settings", "setOnPreferenceClickListener") + findNavController().navigate(R.id.nav_action_settings_widget) + false + } + + // Update Interval + val workInterval = findPreference("work_interval") + workInterval?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + workInterval?.setOnPreferenceChangeListener { _, newValue -> + Log.d("work_interval", "newValue: $newValue") + ctx.updateWorkManager(workInterval, newValue) + } + // Toggle Analytics val analyticsEnabled = findPreference("analytics_enabled") analyticsEnabled?.setOnPreferenceChangeListener { _, newValue -> @@ -104,6 +126,40 @@ class SettingsFragment : PreferenceFragmentCompat() { //Log.d("SettingsFragment", "enableBiometrics: $enableBiometrics") } + fun Context.updateWorkManager(listPref: ListPreference, newValue: Any): Boolean { + Log.d("updateWorkManager", "listPref: ${listPref.value} - newValue: $newValue") + val value = newValue as? String + Log.d("updateWorkManager", "String value: $value") + if (value.isNullOrEmpty()) { + Log.w("updateWorkManager", "NULL OR EMPTY - false") + return false + } else if (listPref.value == value) { + Log.i("updateWorkManager", "NO CHANGE - false") + return false + } else { + Log.i("updateWorkManager", "RESCHEDULING WORK - true") + val interval = value.toLongOrNull() + Log.i("updateWorkManager", "interval: $interval") + if (interval == null || interval == 0L) { + Log.i("updateWorkManager", "DISABLING WORK") + WorkManager.getInstance(this).cancelUniqueWork("app_worker") + return true + } else { + val newRequest = + PeriodicWorkRequestBuilder(interval, TimeUnit.MINUTES) + .setInitialDelay(interval, TimeUnit.MINUTES) + .setConstraints(APP_WORKER_CONSTRAINTS) + .build() + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "app_worker", + ExistingPeriodicWorkPolicy.REPLACE, + newRequest + ) + return true + } + } + } + fun Context.toggleAnalytics(switchPreference: SwitchPreferenceCompat, newValue: Any) { Log.d("toggleAnalytics", "newValue: $newValue") if (newValue as Boolean) { diff --git a/app/src/main/java/org/cssnr/zipline/ui/settings/WidgetSettingsFragment.kt b/app/src/main/java/org/cssnr/zipline/ui/settings/WidgetSettingsFragment.kt new file mode 100644 index 0000000..9985f6c --- /dev/null +++ b/app/src/main/java/org/cssnr/zipline/ui/settings/WidgetSettingsFragment.kt @@ -0,0 +1,42 @@ +package org.cssnr.zipline.ui.settings + +import android.os.Bundle +import android.util.Log +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SeekBarPreference +import org.cssnr.zipline.R + +class WidgetSettingsFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + Log.d("WidgetSettingsFragment", "rootKey: $rootKey") + setPreferencesFromResource(R.xml.preferences_widget, rootKey) + + // Text Color + val textColor = findPreference("widget_text_color") + Log.d("WidgetSettingsFragment", "textColor: $textColor") + textColor?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + + // BG Color + val bgColor = findPreference("widget_bg_color") + Log.d("WidgetSettingsFragment", "bgColor: $bgColor") + bgColor?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + + // BG Opacity + val bgOpacity = preferenceManager.sharedPreferences?.getInt("widget_bg_opacity", 25) + Log.d("WidgetSettingsFragment", "bgOpacity: $bgOpacity") + val seekBar = findPreference("widget_bg_opacity") + seekBar?.summary = "Current Value: $bgOpacity" + seekBar?.apply { + setOnPreferenceChangeListener { pref, newValue -> + val intValue = (newValue as Int) + var stepped = ((intValue + 2) / 5) * 5 + Log.d("WidgetSettingsFragment", "stepped: $stepped") + value = stepped + pref.summary = "Current Value: $stepped" + false + } + } + } +} diff --git a/app/src/main/java/org/cssnr/zipline/ui/upload/TextFragment.kt b/app/src/main/java/org/cssnr/zipline/ui/upload/TextFragment.kt index 7361c3b..97036ef 100644 --- a/app/src/main/java/org/cssnr/zipline/ui/upload/TextFragment.kt +++ b/app/src/main/java/org/cssnr/zipline/ui/upload/TextFragment.kt @@ -55,7 +55,7 @@ class TextFragment : Fragment() { navController = findNavController() val extraText = arguments?.getString("text")?.trim() ?: "" - Log.d("Text[onViewCreated]", "extraText: $extraText") + Log.d("Text[onViewCreated]", "extraText: ${extraText.take(100)}") if (extraText.isEmpty()) { // TODO: Better Handle this Error diff --git a/app/src/main/java/org/cssnr/zipline/widget/WidgetConfiguration.kt b/app/src/main/java/org/cssnr/zipline/widget/WidgetConfiguration.kt new file mode 100644 index 0000000..5cc7ddf --- /dev/null +++ b/app/src/main/java/org/cssnr/zipline/widget/WidgetConfiguration.kt @@ -0,0 +1,127 @@ +package org.cssnr.zipline.widget + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.RadioGroup +import android.widget.SeekBar +import android.widget.TextView +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import org.cssnr.zipline.R + +class WidgetConfiguration : Activity() { + + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.widget_configure) + + setResult(RESULT_CANCELED) + + val intent = intent + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) finish() + + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val bgColor = preferences.getString("widget_bg_color", null) ?: "black" + Log.i("WidgetConfiguration", "bgColor: $bgColor") + val textColor = preferences.getString("widget_text_color", null) ?: "white" + Log.i("WidgetConfiguration", "textColor: $textColor") + val bgOpacity = preferences.getInt("widget_bg_opacity", 35) + Log.i("WidgetConfiguration", "bgOpacity: $bgOpacity") + + val bgOpacityText = findViewById(R.id.bg_opacity_percent) + bgOpacityText.text = getString(R.string.background_opacity, bgOpacity) + + val bgColorId = mapOf( + "white" to R.id.option_white, + "black" to R.id.option_black, + "liberty" to R.id.option_liberty, + ) + val textColorId = mapOf( + "white" to R.id.text_white, + "black" to R.id.text_black, + "liberty" to R.id.text_liberty, + ) + + val backgroundOptions = findViewById(R.id.background_options) + val bgSelected = bgColorId[bgColor] + if (bgSelected != null) backgroundOptions.check(bgSelected) + val textOptions = findViewById(R.id.text_options) + val textSelected = textColorId[textColor] + if (textSelected != null) textOptions.check(textSelected) + + val seekBar = findViewById(R.id.opacity_percent) + seekBar.progress = bgOpacity + seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + if (fromUser && seekBar != null) { + val stepped = ((progress + 2) / 5) * 5 + seekBar.progress = stepped + bgOpacityText.text = getString(R.string.background_opacity, stepped) + Log.d("onProgressChanged", "stepped: $stepped") + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + Log.d("onProgressChanged", "START") + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + Log.d("onProgressChanged", "STOP") + } + }) + + val confirmButton = findViewById