From 496de988699ace21b431932034e249d39c597181 Mon Sep 17 00:00:00 2001 From: Arpit Agarwal <9121467+arpitagarwal1301@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:01:21 +0530 Subject: [PATCH 1/2] Add profile pictures from files --- .../cloudstream3/ui/account/AccountHelper.kt | 67 +++++++++++++++++-- .../cloudstream3/utils/DataStoreHelper.kt | 5 +- .../main/res/layout/account_edit_dialog.xml | 8 ++- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 7725bad9128..ed401439232 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -3,12 +3,16 @@ package com.lagradost.cloudstream3.ui.account import android.app.Activity import android.content.Context import android.content.DialogInterface +import android.content.Intent import android.os.Bundle import android.text.Editable import android.view.LayoutInflater import android.view.inputmethod.EditorInfo import android.widget.TextView import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -43,6 +47,38 @@ import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod import com.lagradost.cloudstream3.utils.UIHelper.showProgress object AccountHelper { + private fun pickProfileImage(context: Context, callback: (String) -> Unit) { + val activity = context.getActivity() as? ComponentActivity ?: return + var launcher: ActivityResultLauncher>? = null + try { + launcher = activity.activityResultRegistry.register( + "profile_image_${System.nanoTime()}", + ActivityResultContracts.OpenDocument() + ) { uri -> + activity.window.decorView.post { launcher?.unregister() } + if (uri == null) return@register + try { + context.contentResolver.takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (error: Exception) { + logError(error) + return@register showToast(R.string.edit_profile_image_error_invalid) + } + ImageLoader(context).enqueue(ImageRequest.Builder(context).data(uri) + .allowHardware(false).size(512, 512).listener( + onSuccess = { _, _ -> callback(uri.toString()) }, + onError = { _, _ -> showToast(R.string.edit_profile_image_error_invalid) } + ).build()) + } + launcher.launch(arrayOf("image/png", "image/jpeg")) + } catch (error: Exception) { + launcher?.unregister() + logError(error) + showToast(R.string.edit_profile_image_error_invalid, Toast.LENGTH_SHORT) + } + } + fun showAccountEditDialog( context: Context, account: DataStoreHelper.Account, @@ -166,7 +202,7 @@ object AccountHelper { canSetPin = true - binding.editProfilePhotoButton.setOnClickListener { + val showProfileImageUrlDialog = { callback: (String) -> Unit -> val bottomSheetDialog = BottomSheetDialog(context) val sheetBinding = BottomInputDialogBinding.inflate(LayoutInflater.from(context)) bottomSheetDialog.setContentView(sheetBinding.root) @@ -189,8 +225,7 @@ object AccountHelper { .allowHardware(false) .listener( onSuccess = { _, _ -> - currentEditAccount = currentEditAccount.copy(customImage = url) - binding.accountImage.loadImage(url) + callback(url) showToast( R.string.edit_profile_image_success, Toast.LENGTH_SHORT @@ -216,6 +251,30 @@ object AccountHelper { } } } + + binding.editProfilePhotoButton.setOnClickListener { + AlertDialog.Builder(context) + .setTitle(R.string.edit_profile_image_title) + .setItems(arrayOf( + context.getString(R.string.player_load_subtitles), + context.getString(R.string.edit_profile_image_hint), + )) { _, selection -> + if (selection == 0) { + pickProfileImage(context) { image -> + if (!dialog.isShowing) return@pickProfileImage + currentEditAccount = currentEditAccount.copy(customImage = image) + binding.accountImage.loadImage(image) + showToast(R.string.edit_profile_image_success, Toast.LENGTH_SHORT) + } + } else { + showProfileImageUrlDialog { image -> + currentEditAccount = currentEditAccount.copy(customImage = image) + binding.accountImage.loadImage(image) + } + } + } + .show() + } } fun showPinInputDialog( @@ -416,4 +475,4 @@ object AccountHelper { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 340266f6c52..37bfb37ba81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -169,7 +169,10 @@ object DataStoreHelper { @JsonProperty("lockPin") @SerialName("lockPin") val lockPin: String? = null, ) { @get:JsonIgnore - val image get() = customImage?.let { UiImage.Image(it) } ?: + val image get() = customImage?.takeUnless { image -> + image.startsWith("content://") && context?.contentResolver?.persistedUriPermissions + ?.none { it.uri.toString() == image } != false + }?.let { UiImage.Image(it) } ?: profileImages.getOrNull(defaultImageIndex)?.let { UiImage.Drawable(it) } ?: UiImage.Drawable(profileImages.first()) diff --git a/app/src/main/res/layout/account_edit_dialog.xml b/app/src/main/res/layout/account_edit_dialog.xml index f52c8ea5196..df8f9e8c002 100644 --- a/app/src/main/res/layout/account_edit_dialog.xml +++ b/app/src/main/res/layout/account_edit_dialog.xml @@ -51,6 +51,7 @@ android:contentDescription="@string/preview_background_img_des" android:focusable="true" android:foreground="@drawable/outline_drawable_forced_round" + android:nextFocusRight="@id/edit_profile_photo_button" android:scaleType="centerCrop" android:src="@drawable/profile_bg_blue" /> @@ -60,6 +61,11 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:background="@color/skipOpTransparent" + android:contentDescription="@string/edit_profile_image_title" + android:focusable="true" + android:foreground="@drawable/outline_drawable_forced_round" + android:nextFocusLeft="@id/account_image" + android:nextFocusDown="@id/account_name" android:padding="5dp" android:src="@drawable/ic_baseline_edit_24" /> @@ -127,4 +133,4 @@ android:text="@string/sort_cancel" /> - \ No newline at end of file + From f59858c734ff4cd7e5883ec5a180d8eae447e5bd Mon Sep 17 00:00:00 2001 From: Arpit Agarwal <9121467+arpitagarwal1301@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:50:28 +0530 Subject: [PATCH 2/2] Use lifecycle-aware profile image picker --- .../lagradost/cloudstream3/MainActivity.kt | 11 +++ .../cloudstream3/ui/account/AccountAdapter.kt | 8 +- .../cloudstream3/ui/account/AccountHelper.kt | 79 +++++++++++-------- .../ui/account/AccountSelectActivity.kt | 14 +++- .../cloudstream3/ui/home/HomeFragment.kt | 7 +- .../ui/home/HomeParentItemAdapterPreview.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 89 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5d39f6554cd..9a2a145426a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.Toast import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.IdRes import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog @@ -104,6 +105,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear +import com.lagradost.cloudstream3.ui.account.ProfileImagePicker import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.library.LibraryViewModel @@ -440,6 +442,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + private val profileImagePicker = ProfileImagePicker(this) + private val profileImageLauncher = registerForActivityResult( + ActivityResultContracts.OpenDocument(), + profileImagePicker::onImagePicked, + ) + + internal fun pickProfileImage(callback: (String) -> Unit) { + profileImagePicker.launch(profileImageLauncher, callback) + } var lastPopup: SearchResponse? = null var lastPopupJob: Job? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt index 92d33d0f349..f18035117e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -23,7 +23,8 @@ class AccountAdapter( private val accountSelectCallback: (DataStoreHelper.Account) -> Unit, private val accountCreateCallback: (DataStoreHelper.Account) -> Unit, private val accountEditCallback: (DataStoreHelper.Account) -> Unit, - private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit + private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit, + private val pickProfileImage: ((String) -> Unit) -> Unit, ) : NoStateAdapter() { companion object { @@ -74,6 +75,7 @@ class AccountAdapter( context = root.context, account = item, isNewAccount = false, + pickProfileImage = pickProfileImage, accountEditCallback = { account -> accountEditCallback.invoke( account @@ -127,6 +129,7 @@ class AccountAdapter( context = root.context, account = item, isNewAccount = false, + pickProfileImage = pickProfileImage, accountEditCallback = { account -> accountEditCallback.invoke(account) }, accountDeleteCallback = { account -> accountDeleteCallback.invoke( @@ -168,6 +171,7 @@ class AccountAdapter( defaultImageIndex = image ), isNewAccount = true, + pickProfileImage = pickProfileImage, accountEditCallback = { account -> accountCreateCallback.invoke(account) }, accountDeleteCallback = {} ) @@ -208,4 +212,4 @@ class AccountAdapter( } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index ed401439232..6cbf6141b5f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -4,15 +4,14 @@ import android.app.Activity import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Editable import android.view.LayoutInflater import android.view.inputmethod.EditorInfo import android.widget.TextView import android.widget.Toast -import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -46,43 +45,59 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod import com.lagradost.cloudstream3.utils.UIHelper.showProgress -object AccountHelper { - private fun pickProfileImage(context: Context, callback: (String) -> Unit) { - val activity = context.getActivity() as? ComponentActivity ?: return - var launcher: ActivityResultLauncher>? = null +internal class ProfileImagePicker(private val context: Context) { + private var callback: ((String) -> Unit)? = null + + fun launch( + launcher: ActivityResultLauncher>, + callback: (String) -> Unit, + ) { + this.callback = callback try { - launcher = activity.activityResultRegistry.register( - "profile_image_${System.nanoTime()}", - ActivityResultContracts.OpenDocument() - ) { uri -> - activity.window.decorView.post { launcher?.unregister() } - if (uri == null) return@register - try { - context.contentResolver.takePersistableUriPermission( - uri, Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - } catch (error: Exception) { - logError(error) - return@register showToast(R.string.edit_profile_image_error_invalid) - } - ImageLoader(context).enqueue(ImageRequest.Builder(context).data(uri) - .allowHardware(false).size(512, 512).listener( - onSuccess = { _, _ -> callback(uri.toString()) }, - onError = { _, _ -> showToast(R.string.edit_profile_image_error_invalid) } - ).build()) - } - launcher.launch(arrayOf("image/png", "image/jpeg")) + launcher.launch(arrayOf("image/*")) } catch (error: Exception) { - launcher?.unregister() + this.callback = null logError(error) showToast(R.string.edit_profile_image_error_invalid, Toast.LENGTH_SHORT) } } + fun onImagePicked(uri: Uri?) { + val callback = callback ?: return + this.callback = null + if (uri == null) return + + try { + context.contentResolver.takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (error: Exception) { + logError(error) + showToast(R.string.edit_profile_image_error_invalid) + return + } + + ImageLoader(context).enqueue( + ImageRequest.Builder(context).data(uri) + .allowHardware(false).size(512, 512).listener( + onSuccess = { _, _ -> + callback(uri.toString()) + showToast(R.string.edit_profile_image_success, Toast.LENGTH_SHORT) + }, + onError = { _, _ -> + showToast(R.string.edit_profile_image_error_invalid) + } + ).build() + ) + } +} + +object AccountHelper { fun showAccountEditDialog( context: Context, account: DataStoreHelper.Account, isNewAccount: Boolean, + pickProfileImage: ((String) -> Unit) -> Unit, accountEditCallback: (DataStoreHelper.Account) -> Unit, accountDeleteCallback: (DataStoreHelper.Account) -> Unit ) { @@ -256,15 +271,14 @@ object AccountHelper { AlertDialog.Builder(context) .setTitle(R.string.edit_profile_image_title) .setItems(arrayOf( - context.getString(R.string.player_load_subtitles), + context.getString(R.string.edit_profile_image_from_file), context.getString(R.string.edit_profile_image_hint), )) { _, selection -> if (selection == 0) { - pickProfileImage(context) { image -> + pickProfileImage { image -> if (!dialog.isShowing) return@pickProfileImage currentEditAccount = currentEditAccount.copy(customImage = image) binding.accountImage.loadImage(image) - showToast(R.string.edit_profile_image_success, Toast.LENGTH_SHORT) } } else { showProfileImageUrlDialog { image -> @@ -463,7 +477,8 @@ object AccountHelper { }, accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) }, accountEditCallback = { viewModel.handleAccountUpdate(it, activity) }, - accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) } + accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) }, + pickProfileImage = activity::pickProfileImage, ).apply { submitList(liveAccounts) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index ad323c7d124..c170ec9bdf5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.account import android.annotation.SuppressLint import android.os.Bundle import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.FragmentActivity import androidx.activity.viewModels import androidx.preference.PreferenceManager @@ -44,6 +45,16 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { val accountViewModel: AccountViewModel by viewModels() + private val profileImagePicker = ProfileImagePicker(this) + private val profileImageLauncher = registerForActivityResult( + ActivityResultContracts.OpenDocument(), + profileImagePicker::onImagePicked, + ) + + private fun pickProfileImage(callback: (String) -> Unit) { + profileImagePicker.launch(profileImageLauncher, callback) + } + @SuppressLint("NotifyDataSetChanged") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -143,7 +154,8 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { navigateToMainActivity() } }, - accountDeleteCallback = { accountViewModel.handleAccountDelete(it, this) } + accountDeleteCallback = { accountViewModel.handleAccountDelete(it, this) }, + pickProfileImage = ::pickProfileImage, ).apply { submitList(liveAccounts) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 96c5f9f565e..9d0f30f4053 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType @@ -658,7 +659,11 @@ class HomeFragment : BaseFragment( } homeMasterAdapter = HomeParentItemAdapterPreview( - homeViewModel, accountViewModel + homeViewModel, + accountViewModel, + pickProfileImage = { callback -> + (activity as? MainActivity)?.pickProfileImage(callback) + }, ) homeMasterRecycler.setRecycledViewPool(ParentItemAdapter.sharedPool) homeMasterRecycler.adapter = homeMasterAdapter diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 959806e566c..ec316a6988a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -64,7 +64,8 @@ import com.lagradost.cloudstream3.ui.setRecycledViewPool class HomeParentItemAdapterPreview( private val viewModel: HomeViewModel, - private val accountViewModel: AccountViewModel + private val accountViewModel: AccountViewModel, + private val pickProfileImage: ((String) -> Unit) -> Unit, ) : ParentItemAdapter( id = "HomeParentItemAdapterPreview".hashCode(), clickCallback = { @@ -104,7 +105,7 @@ class HomeParentItemAdapterPreview( ) } - return HeaderViewHolder(binding, viewModel, accountViewModel) + return HeaderViewHolder(binding, viewModel, accountViewModel, pickProfileImage) } override fun onBindHeader(holder: ViewHolderState) { @@ -131,6 +132,7 @@ class HomeParentItemAdapterPreview( val binding: ViewBinding, val viewModel: HomeViewModel, accountViewModel: AccountViewModel, + private val pickProfileImage: ((String) -> Unit) -> Unit, ) : ViewHolderState(binding) { @@ -558,6 +560,7 @@ class HomeParentItemAdapterPreview( context = context, account = currentAccount, isNewAccount = false, + pickProfileImage = pickProfileImage, accountEditCallback = { accountViewModel.handleAccountUpdate(it, context) }, accountDeleteCallback = { accountViewModel.handleAccountDelete( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf5f..aae373930cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -749,6 +749,7 @@ LongPress Speed Toggle Hold to get 2x speed Edit Profile Image + @string/player_load_subtitles Enter Profile Image URL No URL Found Invalid URL or Image