From 66a08b60b8b796490ed3c0ecd03338c15d331b1b Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 21:14:22 +0100 Subject: [PATCH 01/46] Update build.yaml --- .github/workflows/build.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6c01f4ac3..ea837ecd0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,24 +16,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 - name: Run tests run: ./gradlew ktfmtCheck build: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 - name: Retrieve keystore for apk signing env: ENCODED_KEYSTORE: ${{ secrets.KEYSTORE }} @@ -54,12 +58,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 - name: Retrieve keystore for apk signing env: ENCODED_KEYSTORE: ${{ secrets.KEYSTORE }} From 38629d88f1e3cd631b506fd9ae5b16c6683fd777 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 21:26:41 +0100 Subject: [PATCH 02/46] updated libraries --- app/app/build.gradle.kts | 1 - app/gradle/libs.versions.toml | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 2059edd4a..ae3022791 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -134,7 +134,6 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { jvmTarget = "11" } buildFeatures { viewBinding = true buildConfig = true diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index d3cd49749..cb3870a6f 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -android_gradle_plugin = "8.13.1" +android_gradle_plugin = "8.13.2" appcompat = "1.7.1" constraint_layout = "2.2.1" coil = "2.7.0" core_ktx = "1.17.0" coroutines = "1.10.2" # non compliant with new Play Store but it's not used in production -countly = "25.4.7" +countly = "25.4.9" dagger = "2.57.2" datetime = "0.7.1" espresso = "3.7.0" @@ -16,19 +16,19 @@ fragment = "1.8.9" hilt_navigation = "1.3.0" jackson = "2.20.1" jakarta_xml = "4.0.4" -jsoup = "1.21.2" +jsoup = "1.22.1" junit = "4.13.2" -kotlin = "2.2.21" +kotlin = "2.3.0" ktfmt = "0.25.0" -ksp = "2.3.3" +ksp = "2.3.4" lifecycle = "2.10.0" material_three = "1.13.0" moshi = "1.15.2" navigation = "2.9.6" okhttp = "5.3.2" paging = "3.3.6" -protobuf = "4.33.1" -protobuf_plugin = "0.9.5" +protobuf = "4.33.3" +protobuf_plugin = "0.9.6" preference = "1.2.1" recyclerview = "1.4.0" recyclerview_selection = "1.2.0" @@ -37,7 +37,7 @@ robolectric = "4.16" room = "2.8.4" statemachine = "0.2.0" stax = "1.0-2" -swiperefresh = "1.1.0" +swiperefresh = "1.2.0" test = "1.7.0" test_truth = "1.7.0" test_runner = "1.7.0" From 8cb75bf13869733f0f6fe9bcf7f543fd900debf4 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 21:26:54 +0100 Subject: [PATCH 03/46] added mega.nz support close #445 --- app/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index b6c5551aa..e382d2d55 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -353,6 +353,7 @@ + From ee47a24d3790f094408245325b0932d613af23bc Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 21:27:43 +0100 Subject: [PATCH 04/46] removed youtube support close #447 --- app/app/src/main/AndroidManifest.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index e382d2d55..3e046e572 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -429,10 +429,6 @@ - - - - From f7200be1bfa13e23036e251f091d680bd5071c3b Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 21:43:13 +0100 Subject: [PATCH 05/46] formatted code --- app/app/build.gradle.kts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index ae3022791..b0873c5ff 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -12,10 +12,9 @@ plugins { alias(libs.plugins.room) } -fun readProperties(propertiesFile: File) = - Properties().apply { - if (propertiesFile.exists()) propertiesFile.inputStream().use { fis -> load(fis) } - } +fun readProperties(propertiesFile: File) = Properties().apply { + if (propertiesFile.exists()) propertiesFile.inputStream().use { fis -> load(fis) } +} val keyPropertiesFile: File = rootProject.file("signingkey.properties") val keyProperties = readProperties(keyPropertiesFile) @@ -87,8 +86,7 @@ android { buildTypes { applicationVariants.forEach { variant -> - variant.outputs - .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + variant.outputs.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .forEach { it.outputFileName = "${variant.name}-${variant.versionName}.apk" } } @@ -102,10 +100,8 @@ android { "COUNTLY_APP_KEY", apiProperties.getOrDefault( "COUNTLY_APP_KEY", - "\"" + - (System.getenv("COUNTLY_APP_KEY") - ?: "pDJz4WrY9XeBotXAaL9MYrraSwZNyDqfAPy8p38c") + - "\"", + "\"" + (System.getenv("COUNTLY_APP_KEY") + ?: "pDJz4WrY9XeBotXAaL9MYrraSwZNyDqfAPy8p38c") + "\"", ) as String, ) From cee9d2c47143b4d54565ed59bc055172b95f3e7b Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:02:54 +0100 Subject: [PATCH 06/46] fixed jvm target --- app/app/build.gradle.kts | 4 ++++ app/settings.gradle.kts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index b0873c5ff..e964d91f1 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -34,6 +34,10 @@ ktfmt { kotlinLangStyle() } +kotlin { + jvmToolchain(11) +} + android { namespace = "com.github.livingwithhippos.unchained" compileSdk = 36 diff --git a/app/settings.gradle.kts b/app/settings.gradle.kts index 275f6eaad..0662c6c4a 100644 --- a/app/settings.gradle.kts +++ b/app/settings.gradle.kts @@ -11,6 +11,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { From 590f5d5e35671ec1cc7e830aa6936ccc502ce37d Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:20:23 +0100 Subject: [PATCH 07/46] fixed credentials lost on rotate close #446 --- .../view/AuthenticationFragment.kt | 31 +++++++++---------- .../viewmodel/AuthenticationViewModel.kt | 5 ++- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt index c6179b927..023f6101e 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt @@ -148,28 +148,27 @@ class AuthenticationFragment : UnchainedFragment() { } // 1. start checking for the auth link - viewModel.authLiveData.observe( - viewLifecycleOwner, - EventObserver { auth -> - if (auth != null) { - binding.tvAuthenticationLink.text = auth.verificationUrl - binding.tvAuthenticationLink.visibility = View.VISIBLE - binding.cbLink.isChecked = true - binding.cbLink.text = getString(R.string.link_loaded) - // let the user copy the user code to enter in the website - binding.tvUserCodeValue.text = auth.userCode - binding.bCopyLink.isEnabled = true - // update the currently saved credentials - activityViewModel.updateCredentialsDeviceCode(auth.deviceCode) - // transition state machine + viewModel.authLiveData.observe(viewLifecycleOwner) { event -> + event?.peekContent()?.let { auth -> + binding.tvAuthenticationLink.text = auth.verificationUrl + binding.tvAuthenticationLink.visibility = View.VISIBLE + binding.cbLink.isChecked = true + binding.cbLink.text = getString(R.string.link_loaded) + // let the user copy the user code to enter in the website + binding.tvUserCodeValue.text = auth.userCode + binding.bCopyLink.isEnabled = true + // update the currently saved credentials + activityViewModel.updateCredentialsDeviceCode(auth.deviceCode) + // transition state machine + if (activityViewModel.getAuthenticationMachineState() is FSMAuthenticationState.StartNewLogin) { activityViewModel.transitionAuthenticationMachine( FSMAuthenticationEvent.OnAuthLoaded ) // set up values for calling the secrets endpoint viewModel.setupSecretLoop(auth.expiresIn) } - }, - ) + } + } // 2. start checking for user confirmation viewModel.secretLiveData.observe( diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt index 9c4320a45..100e41ea5 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/viewmodel/AuthenticationViewModel.kt @@ -81,9 +81,8 @@ constructor( * secrets endpoint */ fun setupSecretLoop(expiresIn: Int) { - // this is just an estimate, keeping track of time would be more precise. As of now this - // value - // should be 120 + // this is just an estimate, keeping track of time would be more precise. + // As of now this value should be 120 var calls = (expiresIn * 1000 / SECRET_CALLS_DELAY).toInt() - 10 // remove 10% of the calls to account for the api calls calls -= calls / 10 From 47fd7a55e5781b95a97e5bbc1a5cfdf40196a67b Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:20:35 +0100 Subject: [PATCH 08/46] fixed authentication view in landscape --- app/app/src/main/res/layout/fragment_authentication.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/app/src/main/res/layout/fragment_authentication.xml b/app/app/src/main/res/layout/fragment_authentication.xml index acd9a1da9..db0903604 100644 --- a/app/app/src/main/res/layout/fragment_authentication.xml +++ b/app/app/src/main/res/layout/fragment_authentication.xml @@ -5,12 +5,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:fitsSystemWindows="true" android:orientation="vertical"> + android:layout_height="wrap_content"> Date: Sun, 11 Jan 2026 22:21:22 +0100 Subject: [PATCH 09/46] formatted code --- app/app/build.gradle.kts | 20 ++++++++++--------- .../view/AuthenticationFragment.kt | 14 ++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index e964d91f1..305280782 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -12,9 +12,10 @@ plugins { alias(libs.plugins.room) } -fun readProperties(propertiesFile: File) = Properties().apply { - if (propertiesFile.exists()) propertiesFile.inputStream().use { fis -> load(fis) } -} +fun readProperties(propertiesFile: File) = + Properties().apply { + if (propertiesFile.exists()) propertiesFile.inputStream().use { fis -> load(fis) } + } val keyPropertiesFile: File = rootProject.file("signingkey.properties") val keyProperties = readProperties(keyPropertiesFile) @@ -34,9 +35,7 @@ ktfmt { kotlinLangStyle() } -kotlin { - jvmToolchain(11) -} +kotlin { jvmToolchain(11) } android { namespace = "com.github.livingwithhippos.unchained" @@ -90,7 +89,8 @@ android { buildTypes { applicationVariants.forEach { variant -> - variant.outputs.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + variant.outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .forEach { it.outputFileName = "${variant.name}-${variant.versionName}.apk" } } @@ -104,8 +104,10 @@ android { "COUNTLY_APP_KEY", apiProperties.getOrDefault( "COUNTLY_APP_KEY", - "\"" + (System.getenv("COUNTLY_APP_KEY") - ?: "pDJz4WrY9XeBotXAaL9MYrraSwZNyDqfAPy8p38c") + "\"", + "\"" + + (System.getenv("COUNTLY_APP_KEY") + ?: "pDJz4WrY9XeBotXAaL9MYrraSwZNyDqfAPy8p38c") + + "\"", ) as String, ) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt index 023f6101e..5a6886b7c 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/authentication/view/AuthenticationFragment.kt @@ -101,10 +101,12 @@ class AuthenticationFragment : UnchainedFragment() { val action = AuthenticationFragmentDirections.actionAuthenticationToUser() findNavController().navigate(action) } + FSMAuthenticationState.AuthenticatedPrivateToken -> { val action = AuthenticationFragmentDirections.actionAuthenticationToUser() findNavController().navigate(action) } + FSMAuthenticationState.StartNewLogin -> { // reset the current data // token == null @@ -125,21 +127,26 @@ class AuthenticationFragment : UnchainedFragment() { // get the authentication link to start the process viewModel.fetchAuthenticationInfo() } + FSMAuthenticationState.WaitingUserConfirmation -> { // start the next auth step viewModel.fetchSecrets() } + FSMAuthenticationState.WaitingToken -> { viewModel.fetchToken() } + FSMAuthenticationState.CheckCredentials, FSMAuthenticationState.RefreshingOpenToken -> { // managed by activity } + is FSMAuthenticationState.WaitingUserAction -> { // todo: depending on the action required show an error or restart the // process } + FSMAuthenticationState.Start -> { // this shouldn't happen } @@ -160,7 +167,10 @@ class AuthenticationFragment : UnchainedFragment() { // update the currently saved credentials activityViewModel.updateCredentialsDeviceCode(auth.deviceCode) // transition state machine - if (activityViewModel.getAuthenticationMachineState() is FSMAuthenticationState.StartNewLogin) { + if ( + activityViewModel.getAuthenticationMachineState() + is FSMAuthenticationState.StartNewLogin + ) { activityViewModel.transitionAuthenticationMachine( FSMAuthenticationEvent.OnAuthLoaded ) @@ -185,12 +195,14 @@ class AuthenticationFragment : UnchainedFragment() { FSMAuthenticationEvent.OnUserConfirmationMissing ) } + SecretResult.Expired -> { // will restart the authentication process activityViewModel.transitionAuthenticationMachine( FSMAuthenticationEvent.OnUserConfirmationExpired ) } + is SecretResult.Retrieved -> { if ( activityViewModel.getAuthenticationMachineState() From 37cced7d106b49314f224c2459839decea16e41e Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:35:15 +0100 Subject: [PATCH 10/46] tries to fix null navigation issue see #441 --- .../unchained/base/UnchainedFragment.kt | 22 +++++++++++++++++++ .../unchained/start/view/StartFragment.kt | 19 ---------------- .../user/view/UserProfileFragment.kt | 5 ++--- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/UnchainedFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/UnchainedFragment.kt index 13fd467a1..cfc09538d 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/base/UnchainedFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/base/UnchainedFragment.kt @@ -2,11 +2,33 @@ package com.github.livingwithhippos.unchained.base import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.NavDirections +import androidx.navigation.fragment.findNavController import com.github.livingwithhippos.unchained.start.viewmodel.MainActivityViewModel +import timber.log.Timber /** Base [Fragment] class, giving simple access to the activity ViewModel to its subclasses */ abstract class UnchainedFragment : Fragment() { // activity viewModel. To be used for alerting of expired token or missing network val activityViewModel: MainActivityViewModel by activityViewModels() + + fun safeNavigate(action: NavDirections): Boolean { + val nav = findNavController() + val current = nav.currentDestination + if (current != null && current.getAction(action.actionId) != null) { + try { + nav.navigate(action) + return true + } catch (e: IllegalArgumentException) { + Timber.w(e, "Safe navigate failed for actionId=${action.actionId}") + return false + } + } else { + Timber.w( + "Navigation action not found from destination ${current?.id} for actionId=${action.actionId}" + ) + return false + } + } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt index 64272b17e..033eda680 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt @@ -104,25 +104,6 @@ class StartFragment : UnchainedFragment() { return binding.root } - private fun safeNavigate(action: NavDirections): Boolean { - val nav = findNavController() - val current = nav.currentDestination - if (current != null && current.getAction(action.actionId) != null) { - try { - nav.navigate(action) - return true - } catch (e: IllegalArgumentException) { - Timber.w(e, "Safe navigate failed for actionId=${action.actionId}") - return false - } - } else { - Timber.w( - "Navigation action not found from destination ${current?.id} for actionId=${action.actionId}" - ) - return false - } - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt index 1c453d1ca..73c483f45 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/user/view/UserProfileFragment.kt @@ -11,7 +11,6 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.content.PermissionChecker import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import coil.load import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.base.UnchainedFragment @@ -115,14 +114,14 @@ class UserProfileFragment : UnchainedFragment() { is FSMAuthenticationState.WaitingUserAction -> { // an error occurred, check it and eventually go back to the start fragment val action = UserProfileFragmentDirections.actionUserToStartFragment() - findNavController().navigate(action) + safeNavigate(action) } FSMAuthenticationState.StartNewLogin -> { // the user reset the login, go to the auth fragment val action = UserProfileFragmentDirections.actionUserToAuthenticationFragment() - findNavController().navigate(action) + safeNavigate(action) } FSMAuthenticationState.AuthenticatedOpenToken, From 299b60fce52cee2add765572d57bde366f1182fd Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:38:31 +0100 Subject: [PATCH 11/46] tries to fix more null binding crashes see #441 --- .../livingwithhippos/unchained/lists/view/ListsTabFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt index d072099a9..6e9d91c33 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt @@ -552,6 +552,7 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { viewModel.downloadItemLiveData.observe( viewLifecycleOwner, EventObserver { links -> + if (_binding == null) return@EventObserver // todo: if it gets emptied null/empty should be processed too if (links.isNotEmpty()) { // simulate list refresh @@ -567,6 +568,7 @@ class DownloadsListFragment : UnchainedFragment(), DownloadListListener { activityViewModel.listStateLiveData.observe( viewLifecycleOwner, EventObserver { + if (_binding == null) return@EventObserver when (it) { ListState.UpdateDownload -> { lifecycleScope.launch { From 038237735b904fcd04e1c04335eb12c82d83f3c6 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:39:00 +0100 Subject: [PATCH 12/46] format code --- .../livingwithhippos/unchained/start/view/StartFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt index 033eda680..6acc86e9b 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/view/StartFragment.kt @@ -4,8 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.navigation.NavDirections -import androidx.navigation.fragment.findNavController import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.base.UnchainedFragment import com.github.livingwithhippos.unchained.data.model.UserAction From dde53c91d97b15042c78ebd3af9392fff5568078 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:40:30 +0100 Subject: [PATCH 13/46] trying to fix binding crash --- .../livingwithhippos/unchained/lists/view/ListsTabFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt index 6e9d91c33..79eb46df0 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt @@ -810,6 +810,7 @@ class TorrentsListFragment : UnchainedFragment(), TorrentListListener { activityViewModel.listStateLiveData.observe( viewLifecycleOwner, EventObserver { + if (_binding == null) return@EventObserver when (it) { ListState.UpdateTorrent -> { lifecycleScope.launch { From 23cc796952f763157ef85088c751dfb257eeb73d Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sun, 11 Jan 2026 22:55:03 +0100 Subject: [PATCH 14/46] Update proguard-rules.pro --- app/app/proguard-rules.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/app/app/proguard-rules.pro b/app/app/proguard-rules.pro index f61acb38f..45d5af62b 100644 --- a/app/app/proguard-rules.pro +++ b/app/app/proguard-rules.pro @@ -30,6 +30,7 @@ # kept. Suspend functions are wrapped in continuations where the type argument # is used. -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation +-keep class com.google.re2j.** { *; } # With R8 full mode generic signatures are stripped for classes that are not kept. -keep,allowobfuscation,allowshrinking class retrofit2.Response From 703096cd923195816cd3939405bdc8782ad7be06 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Tue, 3 Feb 2026 23:28:17 +0100 Subject: [PATCH 15/46] ignore case il folder filter search close #451 --- .../unchained/data/service/ForegroundTorrentService.kt | 2 +- .../unchained/folderlist/view/FolderListFragment.kt | 4 ++-- .../unchained/repository/viewmodel/RepositoryViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt index f67e3942c..134a63bdd 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/service/ForegroundTorrentService.kt @@ -89,7 +89,7 @@ class ForegroundTorrentService : LifecycleService() { preferences.getStringSet(KEY_OBSERVED_TORRENTS, emptySet()) as Set // their updated status val newLoadingTorrents = - list.filter { torrent -> loadingStatusList.contains(torrent.status) } + list.filter { torrent -> loadingStatusList.contains(torrent.status.lowercase()) } // the torrent whose status is not a loading one anymore. val finishedTorrents = list diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt index 56aa11f46..2e7196f97 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt @@ -394,12 +394,12 @@ class FolderListFragment : UnchainedFragment(), DownloadListListener { customizedList.addAll(items.filter { it.fileSize > viewModel.getMinFileSize() }) } if (filterType) { - val temp = customizedList.filter { mediaRegex.find(it.filename) != null } + val temp = customizedList.filter { mediaRegex.find(it.filename.lowercase()) != null } customizedList.clear() customizedList.addAll(temp) } if (!filterQuery.isNullOrBlank()) { - val temp = customizedList.filter { item -> item.filename.contains(filterQuery) } + val temp = customizedList.filter { item -> item.filename.contains(filterQuery, ignoreCase = true) } customizedList.clear() customizedList.addAll(temp) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt index 498f4aafa..a366ad222 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt @@ -165,7 +165,7 @@ constructor( val updatablePlugins = remotePlugins.filter { remotePlugin -> val installedVersion: Plugin? = - installedPlugins.firstOrNull { it.name == remotePlugin.plugin } + installedPlugins.firstOrNull { it.name.equals(remotePlugin.plugin, ignoreCase = true) } if (installedVersion == null) false else installedVersion.version < remotePlugin.version From 62f0d3c06e78f649bb4f0723d8fb593d1b73c0f6 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Tue, 3 Feb 2026 23:31:41 +0100 Subject: [PATCH 16/46] Update proguard-rules.pro --- app/app/proguard-rules.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/proguard-rules.pro b/app/app/proguard-rules.pro index 45d5af62b..4c3d2818b 100644 --- a/app/app/proguard-rules.pro +++ b/app/app/proguard-rules.pro @@ -30,7 +30,7 @@ # kept. Suspend functions are wrapped in continuations where the type argument # is used. -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation --keep class com.google.re2j.** { *; } +-dontwarn com.google.re2j.** # With R8 full mode generic signatures are stripped for classes that are not kept. -keep,allowobfuscation,allowshrinking class retrofit2.Response From b626b448c46de952a9290dce191b0bfbf9c7b404 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Tue, 3 Feb 2026 23:32:01 +0100 Subject: [PATCH 17/46] formatting --- .../unchained/folderlist/view/FolderListFragment.kt | 8 ++++++-- .../unchained/repository/viewmodel/RepositoryViewModel.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt index 2e7196f97..df6ec2232 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/folderlist/view/FolderListFragment.kt @@ -394,12 +394,16 @@ class FolderListFragment : UnchainedFragment(), DownloadListListener { customizedList.addAll(items.filter { it.fileSize > viewModel.getMinFileSize() }) } if (filterType) { - val temp = customizedList.filter { mediaRegex.find(it.filename.lowercase()) != null } + val temp = + customizedList.filter { mediaRegex.find(it.filename.lowercase()) != null } customizedList.clear() customizedList.addAll(temp) } if (!filterQuery.isNullOrBlank()) { - val temp = customizedList.filter { item -> item.filename.contains(filterQuery, ignoreCase = true) } + val temp = + customizedList.filter { item -> + item.filename.contains(filterQuery, ignoreCase = true) + } customizedList.clear() customizedList.addAll(temp) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt index a366ad222..143130098 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/repository/viewmodel/RepositoryViewModel.kt @@ -165,7 +165,9 @@ constructor( val updatablePlugins = remotePlugins.filter { remotePlugin -> val installedVersion: Plugin? = - installedPlugins.firstOrNull { it.name.equals(remotePlugin.plugin, ignoreCase = true) } + installedPlugins.firstOrNull { + it.name.equals(remotePlugin.plugin, ignoreCase = true) + } if (installedVersion == null) false else installedVersion.version < remotePlugin.version From da5a53aab93e89e27e804ba8b05ec23315d92b8d Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Wed, 4 Feb 2026 00:34:09 +0100 Subject: [PATCH 18/46] Update proguard-rules.pro --- app/app/proguard-rules.pro | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/app/proguard-rules.pro b/app/app/proguard-rules.pro index 4c3d2818b..d4009dac1 100644 --- a/app/app/proguard-rules.pro +++ b/app/app/proguard-rules.pro @@ -20,11 +20,6 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile -# avoid rewriting of proto datastore variables name. Remove when https://android-review.googlesource.com/c/platform/frameworks/support/+/1433465/ is available --keep class * extends com.google.protobuf.GeneratedMessageLite { - ; - } - # https://github.com/square/retrofit#r8--proguard # With R8 full mode generic signatures are stripped for classes that are not # kept. Suspend functions are wrapped in continuations where the type argument From 38c07af90f7593d6aa866a2375050bd00a4e0320 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Wed, 4 Feb 2026 00:34:19 +0100 Subject: [PATCH 19/46] updated dependencies --- app/gradle/libs.versions.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index cb3870a6f..54603da91 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -14,26 +14,26 @@ datastore = "1.2.0" flexbox = "3.0.0" fragment = "1.8.9" hilt_navigation = "1.3.0" -jackson = "2.20.1" +jackson = "2.21.0" jakarta_xml = "4.0.4" jsoup = "1.22.1" junit = "4.13.2" kotlin = "2.3.0" ktfmt = "0.25.0" -ksp = "2.3.4" +ksp = "2.3.5" lifecycle = "2.10.0" material_three = "1.13.0" moshi = "1.15.2" -navigation = "2.9.6" +navigation = "2.9.7" okhttp = "5.3.2" -paging = "3.3.6" -protobuf = "4.33.3" +paging = "3.4.0" +protobuf = "4.33.5" protobuf_plugin = "0.9.6" preference = "1.2.1" recyclerview = "1.4.0" recyclerview_selection = "1.2.0" retrofit = "3.0.0" -robolectric = "4.16" +robolectric = "4.16.1" room = "2.8.4" statemachine = "0.2.0" stax = "1.0-2" @@ -45,7 +45,7 @@ test_junit = "1.3.0" timber = "5.0.1" viewpager2 = "1.1.0" woodstox = "7.1.1" -work = "2.11.0" +work = "2.11.1" documentfile = "1.1.0" [libraries] From f8d174568d566ec75c56250f6ddb26ad1016dc2a Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Wed, 4 Feb 2026 00:34:57 +0100 Subject: [PATCH 20/46] updated supported links close #447 --- app/app/src/main/AndroidManifest.xml | 75 ++++++---------------------- 1 file changed, 16 insertions(+), 59 deletions(-) diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index 3e046e572..26a76b54a 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -273,144 +273,108 @@ - - - - - + + - + - - - - - - - - - - + - - - - - - - + + + + + - - - - - - - - - - - + + - - + - - + + - - - - - + + - - - - - - - - - @@ -421,15 +385,8 @@ - - - - - - - From 4672d2cbab60dcb5fe9008e2341988a6a64de585 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Fri, 20 Feb 2026 22:59:30 +0100 Subject: [PATCH 21/46] updated libraries --- app/gradle.properties | 10 ++++++++++ app/gradle/gradle-daemon-jvm.properties | 13 +++++++++++++ app/gradle/libs.versions.toml | 10 +++++----- app/gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 app/gradle/gradle-daemon-jvm.properties diff --git a/app/gradle.properties b/app/gradle.properties index df719a2d9..228e34b56 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -17,6 +17,16 @@ org.gradle.jvmargs=-Xms1024m -Xmx4096m -Dfile.encoding=UTF-8 android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library diff --git a/app/gradle/gradle-daemon-jvm.properties b/app/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..2ef1b89d9 --- /dev/null +++ b/app/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/10fc3bf1ee0001078a473afe6e43cfdb/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/9c55677aff3966382f3d853c0959bfb2/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/39846e8427e64a3824c13e399d7d813c/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ac151d55def6b6a9a159dc4cb4642851/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index 54603da91..36fd76536 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -android_gradle_plugin = "8.13.2" +android_gradle_plugin = "9.0.1" appcompat = "1.7.1" constraint_layout = "2.2.1" coil = "2.7.0" core_ktx = "1.17.0" coroutines = "1.10.2" # non compliant with new Play Store but it's not used in production -countly = "25.4.9" +countly = "26.1.0" dagger = "2.57.2" datetime = "0.7.1" espresso = "3.7.0" @@ -15,18 +15,18 @@ flexbox = "3.0.0" fragment = "1.8.9" hilt_navigation = "1.3.0" jackson = "2.21.0" -jakarta_xml = "4.0.4" +jakarta_xml = "4.0.5" jsoup = "1.22.1" junit = "4.13.2" kotlin = "2.3.0" ktfmt = "0.25.0" -ksp = "2.3.5" +ksp = "2.3.6" lifecycle = "2.10.0" material_three = "1.13.0" moshi = "1.15.2" navigation = "2.9.7" okhttp = "5.3.2" -paging = "3.4.0" +paging = "3.4.1" protobuf = "4.33.5" protobuf_plugin = "0.9.6" preference = "1.2.1" diff --git a/app/gradle/wrapper/gradle-wrapper.properties b/app/gradle/wrapper/gradle-wrapper.properties index ad87ea739..647dfb1fb 100644 --- a/app/gradle/wrapper/gradle-wrapper.properties +++ b/app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Jun 05 12:13:53 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 5b056bbf9275e2f7905c9562e2423ee2e800a14c Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Fri, 20 Feb 2026 23:29:26 +0100 Subject: [PATCH 22/46] added docker compose for search services --- .gitignore | 2 ++ extra_assets/docker/docker-compose.yml | 29 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 extra_assets/docker/docker-compose.yml diff --git a/.gitignore b/.gitignore index 42ad7c011..0157ded53 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,5 @@ app/app/src/main/assets/search_plugins/1337.json app/app/src/main/assets/search_plugins/isohunt_nz.json app/app/src/main/assets/search_plugins/zooqle.json app/apikey.properties +/extra_assets/docker/jackett/data +/extra_assets/docker/prowlarr diff --git a/extra_assets/docker/docker-compose.yml b/extra_assets/docker/docker-compose.yml new file mode 100644 index 000000000..037178313 --- /dev/null +++ b/extra_assets/docker/docker-compose.yml @@ -0,0 +1,29 @@ +services: + jackett: + image: lscr.io/linuxserver/jackett:latest + container_name: jackett + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTCdcup + - AUTO_UPDATE=true #optional + - RUN_OPTS= #optional + volumes: + - ./jackett/data:/config + - ./jackett/blackhole:/downloads + ports: + - 9117:9117 + restart: unless-stopped + + prowlarr: + image: lscr.io/linuxserver/prowlarr:latest + container_name: prowlarr + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + volumes: + - ./prowlarr/data:/config + ports: + - 9696:9696 + restart: unless-stopped From ae57a8cacc6716d7b0127d50538a64ca574bc620 Mon Sep 17 00:00:00 2001 From: LivingWithHippos Date: Sat, 21 Feb 2026 17:43:04 +0100 Subject: [PATCH 23/46] WIP added jackett to remote device --- .../remotedevice/view/RemoteDeviceFragment.kt | 6 +- .../view/RemoteServiceFragment.kt | 46 ++++++++-- .../remotedevice/viewmodel/DeviceViewModel.kt | 84 ++++++++++++++++++- .../res/layout/fragment_remote_service.xml | 34 +++++--- .../res/navigation/settings_nav_graph.xml | 6 +- app/app/src/main/res/values/arrays.xml | 2 +- 6 files changed, 151 insertions(+), 27 deletions(-) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceFragment.kt index 4980e13cd..e2cb0dc09 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteDeviceFragment.kt @@ -155,9 +155,7 @@ class RemoteDeviceFragment : UnchainedFragment(), ServiceListListener { R.id.new_remote_service -> { val action = RemoteDeviceFragmentDirections - .actionRemoteDeviceFragmentToRemoteServiceFragment( - deviceID = args.item!!.id - ) + .actionRemoteDeviceFragmentToRemoteServiceFragment(device = args.item!!) findNavController().navigate(action) true } @@ -207,7 +205,7 @@ class RemoteDeviceFragment : UnchainedFragment(), ServiceListListener { val action = RemoteDeviceFragmentDirections.actionRemoteDeviceFragmentToRemoteServiceFragment( item = item, - deviceID = args.item!!.id, + device = args.item!!, ) findNavController().navigate(action) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteServiceFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteServiceFragment.kt index 4cef5ce4c..4702a1557 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteServiceFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/view/RemoteServiceFragment.kt @@ -40,6 +40,7 @@ class RemoteServiceFragment : Fragment() { _binding = FragmentRemoteServiceBinding.inflate(inflater, container, false) val item: RemoteService? = args.item + val deviceID: Int = args.device.id val serviceTypeView = binding.serviceTypePicker.editText as? AutoCompleteTextView @@ -70,6 +71,7 @@ class RemoteServiceFragment : Fragment() { binding.tiUsername.setText(item.username ?: "") binding.tiPassword.setText(item.password.toString()) binding.switchDefault.isChecked = item.isDefault + binding.tiApiToken.setText(item.apiToken) setupServiceType(binding, item.type, serviceTypeView) } @@ -81,11 +83,34 @@ class RemoteServiceFragment : Fragment() { } } + binding.bTestService.setOnClickListener { + val username = binding.tiUsername.text.toString().trim() + val password = binding.tiPassword.text.toString().trim() + val port = binding.tiPort.text.toString().toIntOrNull() + val apiToken = binding.tiApiToken.text.toString().trim() + val serviceType = getServiceType(binding.servicePickerText.text.toString()) + + if (args.device.address.isBlank() || port == null || serviceType == null) { + context?.showToast(R.string.missing_parameter) + } else { + binding.bTestService.isEnabled = false + viewModel.testService( + serviceType, + args.device.address, + port, + username.ifBlank { null }, + password.ifBlank { null }, + apiToken.ifBlank { null }, + ) + } + } + binding.bSaveService.setOnClickListener { val name = binding.tiName.text.toString().trim() val username = binding.tiUsername.text.toString().trim() val password = binding.tiPassword.text.toString().trim() val port = binding.tiPort.text.toString().toIntOrNull() + val apiToken = binding.tiApiToken.text.toString().trim() val serviceId = item?.id ?: 0 if (name.isBlank() || port == null) { @@ -98,13 +123,13 @@ class RemoteServiceFragment : Fragment() { val remoteService = RemoteService( id = serviceId, - device = args.deviceID, + device = deviceID, name = name, port = port, username = username.ifBlank { null }, password = password.ifBlank { null }, type = serviceType.value, - apiToken = binding.tiApiToken.text.toString().trim(), + apiToken = apiToken, isDefault = false, ) viewModel.updateService(remoteService) @@ -114,7 +139,7 @@ class RemoteServiceFragment : Fragment() { val remoteService = RemoteService( id = serviceId, - device = args.deviceID, + device = deviceID, name = name, port = port, username = username.ifBlank { null }, @@ -129,7 +154,7 @@ class RemoteServiceFragment : Fragment() { val remoteService = RemoteService( id = serviceId, - device = args.deviceID, + device = deviceID, name = name, port = port, username = username.ifBlank { null }, @@ -157,7 +182,7 @@ class RemoteServiceFragment : Fragment() { val action = RemoteServiceFragmentDirections.actionRemoteServiceFragmentSelf( item = it.service, - deviceID = args.deviceID, + device = args.device, ) findNavController().navigate(action) } else { @@ -171,6 +196,16 @@ class RemoteServiceFragment : Fragment() { findNavController().popBackStack() } + is DeviceEvent.ServiceWorking -> { + context?.showToast(R.string.connection_successful) + binding.bTestService.isEnabled = true + } + + is DeviceEvent.ServiceNotWorking -> { + context?.showToast(R.string.connection_error) + binding.bTestService.isEnabled = true + } + else -> {} } } @@ -227,6 +262,7 @@ class RemoteServiceFragment : Fragment() { RemoteServiceType.JACKETT -> { binding.switchDefault.isEnabled = false binding.switchDefault.isChecked = false + binding.tfApiToken.visibility = View.VISIBLE } null -> { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/viewmodel/DeviceViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/viewmodel/DeviceViewModel.kt index 91b0b2a2e..8297c2353 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/viewmodel/DeviceViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/remotedevice/viewmodel/DeviceViewModel.kt @@ -5,18 +5,86 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.livingwithhippos.unchained.data.local.RemoteDevice import com.github.livingwithhippos.unchained.data.local.RemoteService +import com.github.livingwithhippos.unchained.data.local.RemoteServiceType import com.github.livingwithhippos.unchained.data.repository.RemoteDeviceRepository +import com.github.livingwithhippos.unchained.di.ClassicClient import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import timber.log.Timber @HiltViewModel -class DeviceViewModel @Inject constructor(private val deviceRepository: RemoteDeviceRepository) : - ViewModel() { +class DeviceViewModel +@Inject +constructor( + private val deviceRepository: RemoteDeviceRepository, + @param:ClassicClient private val client: OkHttpClient, +) : ViewModel() { val deviceLiveData = MutableLiveData() + fun testService( + type: RemoteServiceType, + address: String, + port: Int, + username: String?, + password: String?, + apiToken: String?, + ) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + when (type) { + is RemoteServiceType.JACKETT -> { + val url: StringBuilder = StringBuilder() + if ( + !address.startsWith("http://", ignoreCase = true) && + !address.startsWith("https://", ignoreCase = true) + ) { + if (port == 443) url.append("https://") else url.append("http://") + } + url.append(address) + if (port != 80 && port != 443) url.append(":$port") + url.append("/api/v2.0/indexers/all/results/torznab/api?t=caps") + if (apiToken != null) url.append("&apikey=$apiToken") + val request = okhttp3.Request.Builder().url(url.toString()).build() + try { + val response = client.newCall(request).execute() + if (response.isSuccessful) { + Timber.d(response.body.toString()) + deviceLiveData.postValue(DeviceEvent.ServiceWorking) + } else { + deviceLiveData.postValue( + DeviceEvent.ServiceNotWorking(ServiceErrorType.ResponseError) + ) + } + } catch (e: Exception) { + Timber.e(e, "Error testing the service $url") + deviceLiveData.postValue( + DeviceEvent.ServiceNotWorking(ServiceErrorType.Generic) + ) + } + } + + is RemoteServiceType.KODI -> { + // todo: implement or manage from caller + deviceLiveData.postValue( + DeviceEvent.ServiceNotWorking(ServiceErrorType.InvalidService) + ) + } + + is RemoteServiceType.VLC -> { + deviceLiveData.postValue( + DeviceEvent.ServiceNotWorking(ServiceErrorType.InvalidService) + ) + } + } + } + } + } + fun fetchRemoteDevices() { viewModelScope.launch { deviceLiveData.postValue(DeviceEvent.AllDevices(deviceRepository.getAllDevices())) @@ -100,6 +168,10 @@ class DeviceViewModel @Inject constructor(private val deviceRepository: RemoteDe } sealed class DeviceEvent { + data object ServiceWorking : DeviceEvent() + + data class ServiceNotWorking(val errorType: ServiceErrorType) : DeviceEvent() + data object DeletedAll : DeviceEvent() data object DeletedDevice : DeviceEvent() @@ -119,3 +191,11 @@ sealed class DeviceEvent { data class DeletedService(val service: RemoteService) : DeviceEvent() } + +sealed class ServiceErrorType { + data object ResponseError : ServiceErrorType() + + data object InvalidService : ServiceErrorType() + + data object Generic : ServiceErrorType() +} diff --git a/app/app/src/main/res/layout/fragment_remote_service.xml b/app/app/src/main/res/layout/fragment_remote_service.xml index bf8fe3ce2..0dfa1a1e4 100644 --- a/app/app/src/main/res/layout/fragment_remote_service.xml +++ b/app/app/src/main/res/layout/fragment_remote_service.xml @@ -186,44 +186,52 @@ - - + />