diff --git a/app/build.gradle b/app/build.gradle index eb05c15c..8e1076ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,8 +26,8 @@ android { vectorDrawables.useSupportLibrary false vectorDrawables.generatedDensities = [] - versionCode 3020335 - versionName "7.2.2" + versionCode 3020336 + versionName "7.2.3" ndkVersion "27.0.12077973" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/WifiAuthenticationFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/WifiAuthenticationFragment.kt deleted file mode 100644 index 30b2a9f2..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/WifiAuthenticationFragment.kt +++ /dev/null @@ -1,136 +0,0 @@ -package ac.mdiq.podcini.preferences.fragments - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.WifiSyncDialogBinding -import ac.mdiq.podcini.net.sync.SynchronizationCredentials -import ac.mdiq.podcini.net.sync.SynchronizationSettings.setWifiSyncEnabled -import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort -import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync -import ac.mdiq.podcini.util.EventFlow -import ac.mdiq.podcini.util.FlowEvent -import ac.mdiq.podcini.util.Logd -import android.app.Dialog -import android.content.Context.WIFI_SERVICE -import android.net.wifi.WifiManager -import android.os.Bundle -import android.view.View -import android.widget.Button -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import java.util.Locale - -class WifiAuthenticationFragment : DialogFragment() { - private var binding: WifiSyncDialogBinding? = null - private var portNum = 0 - private var isGuest: Boolean? = null - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = MaterialAlertDialogBuilder(requireContext()) - dialog.setTitle(R.string.connect_to_peer) - dialog.setNegativeButton(R.string.cancel_label, null) - dialog.setPositiveButton(R.string.confirm_label, null) - - binding = WifiSyncDialogBinding.inflate(layoutInflater) - dialog.setView(binding!!.root) - - binding!!.hostAddressText.setText(SynchronizationCredentials.hosturl?:"") - portNum = SynchronizationCredentials.hostport - if (portNum == 0) portNum = hostPort - binding!!.hostPortText.setText(portNum.toString()) - - binding!!.guestButton.setOnClickListener { - binding!!.hostAddressText.visibility = View.VISIBLE - binding!!.hostPortText.visibility = View.VISIBLE - binding!!.hostButton.visibility = View.INVISIBLE - SynchronizationCredentials.hosturl = binding!!.hostAddressText.text.toString() - portNum = binding!!.hostPortText.text.toString().toInt() - isGuest = true - SynchronizationCredentials.hostport = portNum - } - binding!!.hostButton.setOnClickListener { - binding!!.hostAddressText.visibility = View.VISIBLE - binding!!.hostPortText.visibility = View.VISIBLE - binding!!.guestButton.visibility = View.INVISIBLE - val wifiManager = requireContext().applicationContext.getSystemService(WIFI_SERVICE) as WifiManager - val ipAddress = wifiManager.connectionInfo.ipAddress - val ipString = String.format(Locale.US, "%d.%d.%d.%d", ipAddress and 0xff, ipAddress shr 8 and 0xff, ipAddress shr 16 and 0xff, ipAddress shr 24 and 0xff) - binding!!.hostAddressText.setText(ipString) - binding!!.hostAddressText.isEnabled = false - portNum = binding!!.hostPortText.text.toString().toInt() - isGuest = false - SynchronizationCredentials.hostport = portNum - } - procFlowEvents() - return dialog.create() - } - override fun onDestroy() { - cancelFlowEvents() - super.onDestroy() - } - override fun onResume() { - super.onResume() - val d = dialog as? AlertDialog - if (d != null) { - val confirmButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button - confirmButton.setOnClickListener { - Logd(TAG, "confirm button pressed") - if (isGuest == null) { - Toast.makeText(requireContext(), R.string.host_or_guest, Toast.LENGTH_LONG).show() - return@setOnClickListener - } - binding!!.progressContainer.visibility = View.VISIBLE - confirmButton.visibility = View.INVISIBLE - val cancelButton = d.getButton(Dialog.BUTTON_NEGATIVE) as Button - cancelButton.visibility = View.INVISIBLE - portNum = binding!!.hostPortText.text.toString().toInt() - setWifiSyncEnabled(true) - startInstantSync(requireContext(), portNum, binding!!.hostAddressText.text.toString(), isGuest!!) - } - } - } - - private var eventSink: Job? = null - private fun cancelFlowEvents() { - eventSink?.cancel() - eventSink = null - } - private fun procFlowEvents() { - if (eventSink != null) return - eventSink = lifecycleScope.launch { - EventFlow.events.collectLatest { event -> - Logd(TAG, "Received event: ${event.TAG}") - when (event) { - is FlowEvent.SyncServiceEvent -> syncStatusChanged(event) - else -> {} - } - } - } - } - fun syncStatusChanged(event: FlowEvent.SyncServiceEvent) { - when (event.messageResId) { - R.string.sync_status_error -> { - Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG).show() - dialog?.dismiss() - } - R.string.sync_status_success -> { - Toast.makeText(requireContext(), R.string.sync_status_success, Toast.LENGTH_LONG).show() - dialog?.dismiss() - } - R.string.sync_status_in_progress -> binding!!.progressBar.progress = event.message.toInt() - else -> { - Logd(TAG, "Sync result unknow ${event.messageResId}") -// Toast.makeText(context, "Sync result unknow ${event.messageResId}", Toast.LENGTH_LONG).show() - } - } - } - - companion object { - val TAG = WifiAuthenticationFragment::class.simpleName ?: "Anonymous" - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/screens/Synchronization.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/screens/Synchronization.kt index 1569f61d..6c0806d9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/screens/Synchronization.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/screens/Synchronization.kt @@ -1,5 +1,6 @@ package ac.mdiq.podcini.preferences.screens +import ac.mdiq.podcini.PodciniApp.Companion.getAppContext import ac.mdiq.podcini.R import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient import ac.mdiq.podcini.net.sync.SyncService @@ -8,36 +9,46 @@ import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData import ac.mdiq.podcini.net.sync.SynchronizationSettings import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.SynchronizationSettings.setSelectedSyncProvider +import ac.mdiq.podcini.net.sync.SynchronizationSettings.setWifiSyncEnabled import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow.AuthenticationCallback -import ac.mdiq.podcini.preferences.AppPreferences +import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync import ac.mdiq.podcini.preferences.AppPreferences.AppPrefs import ac.mdiq.podcini.preferences.AppPreferences.getPref import ac.mdiq.podcini.preferences.AppPreferences.putPref -import ac.mdiq.podcini.preferences.fragments.WifiAuthenticationFragment import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.compose.CustomTextStyles import ac.mdiq.podcini.ui.compose.CustomToast import ac.mdiq.podcini.ui.compose.IconTitleSummaryActionRow import ac.mdiq.podcini.ui.compose.TitleSummaryActionColumn +import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.Logd +import android.content.Context.WIFI_SERVICE +import android.net.wifi.WifiManager import android.text.format.DateUtils +import android.widget.Toast import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.* @Composable fun SynchronizationPreferencesScreen(activity: PreferenceActivity) { @@ -156,6 +167,113 @@ fun SynchronizationPreferencesScreen(activity: PreferenceActivity) { ) } + @Composable + fun WifiAuthenticationDialog(onDismissRequest: ()->Unit) { + val TAG = "WifiAuthenticationDialog" + val textColor = MaterialTheme.colorScheme.onSurface + val context = LocalContext.current + var progressMessage by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf("") } + val scope = rememberCoroutineScope() + scope.launch { + EventFlow.events.collectLatest { event -> + Logd(TAG, "Received event: ${event.TAG}") + when (event) { + is FlowEvent.SyncServiceEvent -> { + when (event.messageResId) { + R.string.sync_status_error -> { + errorMessage = event.message + Toast.makeText(context, event.message, Toast.LENGTH_LONG).show() + onDismissRequest() + } + R.string.sync_status_success -> { + Toast.makeText(context, R.string.sync_status_success, Toast.LENGTH_LONG).show() + onDismissRequest() + } + R.string.sync_status_in_progress -> progressMessage = event.message + else -> { + Logd(TAG, "Sync result unknow ${event.messageResId}") + } + } + } + else -> {} + } + } + } + var portNum by remember { mutableIntStateOf(SynchronizationCredentials.hostport) } + var isGuest by remember { mutableStateOf(null) } + var hostAddress by remember { mutableStateOf(SynchronizationCredentials.hosturl?:"") } + var showHostAddress by remember { mutableStateOf(true) } + var portString by remember { mutableStateOf(SynchronizationCredentials.hostport.toString()) } + var showProgress by remember { mutableStateOf(false) } + var showConfirm by remember { mutableStateOf(true) } + var showCancel by remember { mutableStateOf(true) } + AlertDialog(modifier = Modifier.fillMaxWidth().border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = { onDismissRequest() }, + title = { Text(stringResource(R.string.connect_to_peer), style = CustomTextStyles.titleCustom) }, + text = { + Column { + Text(stringResource(R.string.wifisync_explanation_message), style = MaterialTheme.typography.bodySmall) + Row { + TextButton(onClick = { + val wifiManager = context.getSystemService(WIFI_SERVICE) as WifiManager + val ipAddress = wifiManager.connectionInfo.ipAddress + val ipString = String.format(Locale.US, "%d.%d.%d.%d", ipAddress and 0xff, ipAddress shr 8 and 0xff, ipAddress shr 16 and 0xff, ipAddress shr 24 and 0xff) + hostAddress = ipString + showHostAddress = false + portNum = portString.toInt() + isGuest = false + SynchronizationCredentials.hostport = portNum + }) { Text(stringResource(R.string.host_butLabel)) } + Spacer(Modifier.weight(1f)) + TextButton(onClick = { + SynchronizationCredentials.hosturl = hostAddress + showHostAddress = true + portNum = portString.toInt() + isGuest = true + SynchronizationCredentials.hostport = portNum + }) { Text(stringResource(R.string.guest_butLabel)) } + } + Row { + if (showHostAddress) TextField(value = hostAddress, modifier = Modifier.weight(0.6f), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> hostAddress = input }, + label = { Text(stringResource(id = R.string.synchronization_host_address_label)) }) + TextField(value = portString, modifier = Modifier.weight(0.4f).padding(start = 3.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + portString = input + portNum = input.toInt() + }, + label = { Text(stringResource(id = R.string.synchronization_host_port_label)) }) + } + if (showProgress) { + CircularProgressIndicator(progress = {0.6f}, strokeWidth = 10.dp, color = textColor, modifier = Modifier.size(50.dp)) + Text(stringResource(R.string.wifisync_progress_message) + " " + progressMessage, color = textColor) + } + Text(errorMessage, style = MaterialTheme.typography.bodyMedium) + } + }, + confirmButton = { + if (showConfirm) TextButton(onClick = { + Logd(TAG, "confirm button pressed") + if (isGuest == null) { + Toast.makeText(getAppContext(), R.string.host_or_guest, Toast.LENGTH_LONG).show() + return@TextButton + } + showProgress = true + showConfirm = false + showCancel = false + setWifiSyncEnabled(true) + startInstantSync(getAppContext(), portNum, hostAddress, isGuest!!) + }) { Text(stringResource(R.string.confirm_label)) } + }, + dismissButton = { if (showCancel) TextButton(onClick = { onDismissRequest() }) { Text(stringResource(R.string.cancel_label)) } } + ) + } + + var ShowWifiAuthenticationDialog by remember { mutableStateOf(false) } + if (ShowWifiAuthenticationDialog) WifiAuthenticationDialog { ShowWifiAuthenticationDialog = false } + var ChooseProviderAndLoginDialog by remember { mutableStateOf(false) } if (ChooseProviderAndLoginDialog) ChooseProviderAndLoginDialog { ChooseProviderAndLoginDialog = false } @@ -168,7 +286,10 @@ fun SynchronizationPreferencesScreen(activity: PreferenceActivity) { val textColor = MaterialTheme.colorScheme.onSurface val scrollState = rememberScrollState() Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).verticalScroll(scrollState)) { - IconTitleSummaryActionRow(R.drawable.wifi_sync, R.string.wifi_sync, R.string.wifi_sync_summary_unchoosen) { WifiAuthenticationFragment().show(activity.supportFragmentManager, WifiAuthenticationFragment.TAG) } + IconTitleSummaryActionRow(R.drawable.wifi_sync, R.string.wifi_sync, R.string.wifi_sync_summary_unchoosen) { + ShowWifiAuthenticationDialog = true +// WifiAuthenticationFragment().show(activity.supportFragmentManager, WifiAuthenticationFragment.TAG) + } Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) { var titleRes by remember { mutableStateOf(0) } var summaryRes by remember { mutableIntStateOf(R.string.synchronization_summary_unchoosen) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index c4914e33..0cfee2ba 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -434,7 +434,7 @@ class PreferenceActivity : AppCompatActivity() { TitleSummarySwitchPrefRow(R.string.pref_show_notification_skip_title, R.string.pref_show_notification_skip_sum, AppPrefs.prefShowSkip.name, true) Text(stringResource(R.string.behavior), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 15.dp)) var showDefaultPageOptions by remember { mutableStateOf(false) } - var tempSelectedOption by remember { mutableStateOf(getPref(AppPrefs.prefDefaultPage, DefaultPages.SubscriptionsFragment.name)!!) } + var tempSelectedOption by remember { mutableStateOf(getPref(AppPrefs.prefDefaultPage, DefaultPages.SubscriptionsFragment.name)) } TitleSummaryActionColumn(R.string.pref_default_page, R.string.pref_default_page_sum) { showDefaultPageOptions = true } if (showDefaultPageOptions) { AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = { showDefaultPageOptions = false }, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index b9203407..a3865056 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -111,62 +111,67 @@ import kotlin.math.max import kotlin.math.sin class AudioPlayerFragment : Fragment() { - val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences("AudioPlayerFragmentPrefs", Context.MODE_PRIVATE) } - - private var isCollapsed by mutableStateOf(true) - - private var controllerFuture: ListenableFuture? = null - private var controller: ServiceStatusHandler? = null - - private var prevItem: Episode? = null - private var currentItem by mutableStateOf(null) - - private var playButInit = false - private var isShowPlay: Boolean = true - - private var showTimeLeft = false - private var titleText by mutableStateOf("") - private var imgLoc by mutableStateOf(null) - private var imgLocLarge by mutableStateOf(null) - private var txtvPlaybackSpeed by mutableStateOf("") - private var curPlaybackSpeed by mutableStateOf(1f) - private var isVideoScreen = false - private var playButRes by mutableIntStateOf(R.drawable.ic_play_48dp) - private var currentPosition by mutableIntStateOf(0) - private var duration by mutableIntStateOf(0) - private var txtvLengtTexth by mutableStateOf("") - private var sliderValue by mutableFloatStateOf(0f) - private var sleepTimerActive by mutableStateOf(isSleepTimerActive()) - private var showSpeedDialog by mutableStateOf(false) - - private var shownotesCleaner: ShownotesCleaner? = null - - private var cleanedNotes by mutableStateOf(null) - private var homeText: String? = null - private var showHomeText = false - private var readerhtml: String? = null - private var txtvPodcastTitle by mutableStateOf("") - private var episodeDate by mutableStateOf("") -// private var chapterControlVisible by mutableStateOf(false) - private var hasNextChapter by mutableStateOf(true) - var rating by mutableStateOf(currentItem?.rating ?: Rating.UNRATED.code) - - private var resetPlayer by mutableStateOf(false) - - val showErrorDialog = mutableStateOf(false) - var errorMessage by mutableStateOf("") - - private var displayedChapterIndex by mutableIntStateOf(-1) - private val currentChapter: Chapter? - get() { - if (currentItem?.chapters.isNullOrEmpty() || displayedChapterIndex == -1) return null - return currentItem!!.chapters[displayedChapterIndex] - } +// val s by lazy { requireContext().getSharedPreferences("AudioPlayerFragmentPrefs", Context.MODE_PRIVATE) } + + class AudioPlayerVM { + internal var isCollapsed by mutableStateOf(true) + + internal var controllerFuture: ListenableFuture? = null + internal var controller: ServiceStatusHandler? = null + + internal var prevItem: Episode? = null + internal var curItem by mutableStateOf(null) + + internal var playButInit = false + internal var isShowPlay: Boolean = true + + internal var showTimeLeft = false + internal var titleText by mutableStateOf("") + internal var imgLoc by mutableStateOf(null) + internal var imgLocLarge by mutableStateOf(null) + internal var txtvPlaybackSpeed by mutableStateOf("") + internal var curPlaybackSpeed by mutableStateOf(1f) + internal var isVideoScreen = false + internal var playButRes by mutableIntStateOf(R.drawable.ic_play_48dp) + internal var curPosition by mutableIntStateOf(0) + internal var duration by mutableIntStateOf(0) + internal var txtvLengtTexth by mutableStateOf("") + internal var sliderValue by mutableFloatStateOf(0f) + internal var sleepTimerActive by mutableStateOf(isSleepTimerActive()) + internal var showSpeedDialog by mutableStateOf(false) + + internal var shownotesCleaner: ShownotesCleaner? = null + + internal var cleanedNotes by mutableStateOf(null) + internal var homeText: String? = null + internal var showHomeText = false + internal var readerhtml: String? = null + internal var txtvPodcastTitle by mutableStateOf("") + internal var episodeDate by mutableStateOf("") + + // private var chapterControlVisible by mutableStateOf(false) + internal var hasNextChapter by mutableStateOf(true) + var rating by mutableStateOf(curItem?.rating ?: Rating.UNRATED.code) + + internal var resetPlayer by mutableStateOf(false) + + val showErrorDialog = mutableStateOf(false) + var errorMessage by mutableStateOf("") + + internal var displayedChapterIndex by mutableIntStateOf(-1) + internal val curChapter: Chapter? + get() { + if (curItem?.chapters.isNullOrEmpty() || displayedChapterIndex == -1) return null + return curItem!!.chapters[displayedChapterIndex] + } + } + + private val vm = AudioPlayerVM() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) Logd(TAG, "fragment onCreateView") - controller = object : ServiceStatusHandler(requireActivity()) { + vm.controller = object : ServiceStatusHandler(requireActivity()) { override fun updatePlayButton(showPlay: Boolean) { setIsShowPlay(showPlay) } @@ -179,37 +184,13 @@ class AudioPlayerFragment : Fragment() { (activity as MainActivity).setPlayerVisible(false) } } - controller!!.init() + vm.controller!!.init() onCollaped() - if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext()) + if (vm.shownotesCleaner == null) vm.shownotesCleaner = ShownotesCleaner(requireContext()) + + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { AudioPlayerScreen() } } } - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f, onDismiss = {showSpeedDialog = false}) - LaunchedEffect(key1 = curMediaId) { - cleanedNotes = null - if (curEpisode != null) { - updateUi(curEpisode!!) - imgLoc = curEpisode!!.getEpisodeListImageLocation() - currentItem = curEpisode - } - } - MediaPlayerErrorDialog(activity as Activity, errorMessage, showErrorDialog) - Box(modifier = Modifier.fillMaxWidth().then(if (isCollapsed) Modifier else Modifier.statusBarsPadding().navigationBarsPadding())) { - PlayerUI(Modifier.align(if (isCollapsed) Alignment.TopCenter else Alignment.BottomCenter).zIndex(1f)) - if (!isCollapsed) { - if (cleanedNotes == null) updateDetails() - Column(Modifier.padding(bottom = 120.dp)) { - Toolbar() - DetailUI(modifier = Modifier) - } - } - } - } - } - } Logd(TAG, "curMedia: ${curEpisode?.id}") (activity as MainActivity).setPlayerVisible(curEpisode != null) if (curEpisode != null) updateUi(curEpisode!!) @@ -218,13 +199,37 @@ class AudioPlayerFragment : Fragment() { override fun onDestroyView() { Logd(TAG, "Fragment destroyed") - controller?.release() - controller = null - if (controllerFuture != null) MediaController.releaseFuture(controllerFuture!!) - controllerFuture = null + vm.controller?.release() + vm.controller = null + if (vm.controllerFuture != null) MediaController.releaseFuture(vm.controllerFuture!!) + vm.controllerFuture = null super.onDestroyView() } + @Composable + fun AudioPlayerScreen() { + if (vm.showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f, onDismiss = {vm.showSpeedDialog = false}) + LaunchedEffect(key1 = curMediaId) { + vm.cleanedNotes = null + if (curEpisode != null) { + updateUi(curEpisode!!) + vm.imgLoc = curEpisode!!.getEpisodeListImageLocation() + vm.curItem = curEpisode + } + } + MediaPlayerErrorDialog(activity as Activity, vm.errorMessage, vm.showErrorDialog) + Box(modifier = Modifier.fillMaxWidth().then(if (vm.isCollapsed) Modifier else Modifier.statusBarsPadding().navigationBarsPadding())) { + PlayerUI(Modifier.align(if (vm.isCollapsed) Alignment.TopCenter else Alignment.BottomCenter).zIndex(1f)) + if (!vm.isCollapsed) { + if (vm.cleanedNotes == null) updateDetails() + Column(Modifier.padding(bottom = 120.dp)) { + Toolbar() + DetailUI(modifier = Modifier) + } + } + } + } + @OptIn(ExperimentalFoundationApi::class) @Composable fun ControlUI() { @@ -251,12 +256,12 @@ class AudioPlayerFragment : Fragment() { if (curEpisode == null) return if (playbackService == null) PlaybackServiceStarter(requireContext(), curEpisode!!).start() } - AsyncImage(contentDescription = "imgvCover", model = ImageRequest.Builder(context).data(imgLoc) + AsyncImage(contentDescription = "imgvCover", model = ImageRequest.Builder(context).data(vm.imgLoc) .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp) .clickable(onClick = { Logd(TAG, "playerUiFragment icon was clicked") - if (isCollapsed) { + if (vm.isCollapsed) { val media = curEpisode if (media != null) { val mediaType = media.getMediaType() @@ -275,9 +280,9 @@ class AudioPlayerFragment : Fragment() { })) Spacer(Modifier.weight(0.1f)) Column(horizontalAlignment = Alignment.CenterHorizontally) { - SpeedometerWithArc(speed = curPlaybackSpeed*100, maxSpeed = 300f, trackColor = textColor, - modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = { showSpeedDialog = true })) - Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall) + SpeedometerWithArc(speed = vm.curPlaybackSpeed*100, maxSpeed = 300f, trackColor = textColor, + modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = { vm.showSpeedDialog = true })) + Text(vm.txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall) } Spacer(Modifier.weight(0.1f)) Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -291,13 +296,13 @@ class AudioPlayerFragment : Fragment() { Text(rewindSecs, color = textColor, style = MaterialTheme.typography.bodySmall) } Spacer(Modifier.weight(0.1f)) - Icon(imageVector = ImageVector.vectorResource(playButRes), tint = textColor, contentDescription = "play", + Icon(imageVector = ImageVector.vectorResource(vm.playButRes), tint = textColor, contentDescription = "play", modifier = Modifier.width(64.dp).height(64.dp).combinedClickable( onClick = { - if (controller == null) return@combinedClickable + if (vm.controller == null) return@combinedClickable if (curEpisode != null) { val media = curEpisode!! - setIsShowPlay(!isShowPlay) + setIsShowPlay(!vm.isShowPlay) if (media.getMediaType() == MediaType.VIDEO && status != PlayerStatus.PLAYING && (media.feed?.videoModePolicy != VideoMode.AUDIO_ONLY)) { playPause() @@ -353,28 +358,28 @@ class AudioPlayerFragment : Fragment() { @Composable fun ProgressBar() { val textColor = MaterialTheme.colorScheme.onSurface - Slider(value = sliderValue, valueRange = 0f..duration.toFloat(), + Slider(value = vm.sliderValue, valueRange = 0f..vm.duration.toFloat(), modifier = Modifier.height(12.dp).padding(top = 2.dp, bottom = 2.dp), onValueChange = { Logd(TAG, "Slider onValueChange: $it") - sliderValue = it + vm.sliderValue = it }, onValueChangeFinished = { - Logd(TAG, "Slider onValueChangeFinished: $sliderValue") - currentPosition = sliderValue.toInt() + Logd(TAG, "Slider onValueChangeFinished: ${vm.sliderValue}") + vm.curPosition = vm.sliderValue.toInt() // if (playbackService?.isServiceReady() == true) seekTo(currentPosition) - seekTo(currentPosition) + seekTo(vm.curPosition) }) Row { - Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodySmall) + Text(DurationConverter.getDurationStringLong(vm.curPosition), color = textColor, style = MaterialTheme.typography.bodySmall) Spacer(Modifier.weight(1f)) val bitrate = curEpisode?.bitrate ?: 0 if (bitrate > 0) Text(formatLargeInteger(bitrate) + "bits", color = textColor, style = MaterialTheme.typography.bodySmall) Spacer(Modifier.weight(1f)) - showTimeLeft = getPref(AppPrefs.showTimeLeft, false) - Text(txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.clickable { - if (controller == null) return@clickable - showTimeLeft = !showTimeLeft - putPref(AppPrefs.showTimeLeft, showTimeLeft) + vm.showTimeLeft = getPref(AppPrefs.showTimeLeft, false) + Text(vm.txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.clickable { + if (vm.controller == null) return@clickable + vm.showTimeLeft = !vm.showTimeLeft + putPref(AppPrefs.showTimeLeft, vm.showTimeLeft) onPositionUpdate(FlowEvent.PlaybackPositionEvent(curEpisode, curPositionFB, curDurationFB)) }) } @@ -384,7 +389,7 @@ class AudioPlayerFragment : Fragment() { fun PlayerUI(modifier: Modifier) { val textColor = MaterialTheme.colorScheme.onSurface Column(modifier = modifier.fillMaxWidth().border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)).background(MaterialTheme.colorScheme.surface)) { - Text(titleText, maxLines = 1, color = textColor, style = MaterialTheme.typography.bodyMedium) + Text(vm.titleText, maxLines = 1, color = textColor, style = MaterialTheme.typography.bodyMedium) ProgressBar() ControlUI() } @@ -392,7 +397,7 @@ class AudioPlayerFragment : Fragment() { @Composable fun VolumeAdaptionDialog(onDismissRequest: () -> Unit) { - val (selectedOption, onOptionSelected) = remember { mutableStateOf(currentItem?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } + val (selectedOption, onOptionSelected) = remember { mutableStateOf(vm.curItem?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } Dialog(onDismissRequest = { onDismissRequest() }) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -403,9 +408,9 @@ class AudioPlayerFragment : Fragment() { Logd(TAG, "row clicked: $item $selectedOption") if (item != selectedOption) { onOptionSelected(item) - if (currentItem != null) { - currentItem?.volumeAdaptionSetting = item - curEpisode = currentItem + if (vm.curItem != null) { + vm.curItem?.volumeAdaptionSetting = item + curEpisode = vm.curItem playbackService?.mPlayer?.pause(reinit = true) playbackService?.mPlayer?.resume() } @@ -431,14 +436,14 @@ class AudioPlayerFragment : Fragment() { var showVolumeDialog by remember { mutableStateOf(false) } if (showVolumeDialog) VolumeAdaptionDialog { showVolumeDialog = false } var showShareDialog by remember { mutableStateOf(false) } - if (showShareDialog && currentItem != null) ShareDialog(currentItem!!, requireActivity()) {showShareDialog = false } + if (showShareDialog && vm.curItem != null) ShareDialog(vm.curItem!!, requireActivity()) {showShareDialog = false } Row(modifier = Modifier.fillMaxWidth().padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_down), tint = textColor, contentDescription = "Collapse", modifier = Modifier.clickable { (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) }) var homeIcon by remember { mutableIntStateOf(R.drawable.baseline_home_24)} Icon(imageVector = ImageVector.vectorResource(homeIcon), tint = textColor, contentDescription = "Home", modifier = Modifier.clickable { - homeIcon = if (showHomeText) R.drawable.ic_home else R.drawable.outline_home_24 + homeIcon = if (vm.showHomeText) R.drawable.ic_home else R.drawable.outline_home_24 buildHomeReaderText() }) if (mediaType == MediaType.VIDEO) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_fullscreen_24), tint = textColor, contentDescription = "Play video", @@ -451,13 +456,13 @@ class AudioPlayerFragment : Fragment() { } VideoPlayerActivityStarter(requireContext()).start() }) - if (controller != null) { - val sleepRes = if (sleepTimerActive) R.drawable.ic_sleep_off else R.drawable.ic_sleep + if (vm.controller != null) { + val sleepRes = if (vm.sleepTimerActive) R.drawable.ic_sleep_off else R.drawable.ic_sleep Icon(imageVector = ImageVector.vectorResource(sleepRes), tint = textColor, contentDescription = "Sleep timer", modifier = Modifier.clickable { SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog") }) } - if (currentItem != null) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_feed), tint = textColor, contentDescription = "Open podcast", + if (vm.curItem != null) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_feed), tint = textColor, contentDescription = "Open podcast", modifier = Modifier.clickable { if (feedItem.feedId != null) { val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId!!) @@ -465,13 +470,13 @@ class AudioPlayerFragment : Fragment() { } }) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_share), tint = textColor, contentDescription = "Share", modifier = Modifier.clickable { - if (currentItem != null) showShareDialog = true + if (vm.curItem != null) showShareDialog = true }) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_volume_adaption), tint = textColor, contentDescription = "Volume adaptation", modifier = Modifier.clickable { - if (currentItem != null) showVolumeDialog = true + if (vm.curItem != null) showVolumeDialog = true }) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable { - val notes = if (showHomeText) readerhtml else feedItem.description + val notes = if (vm.showHomeText) vm.readerhtml else feedItem.description if (!notes.isNullOrEmpty()) { val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() val context = requireContext() @@ -487,9 +492,9 @@ class AudioPlayerFragment : Fragment() { @Composable fun DetailUI(modifier: Modifier) { var showChooseRatingDialog by remember { mutableStateOf(false) } - if (showChooseRatingDialog) ChooseRatingDialog(listOf(currentItem!!)) { showChooseRatingDialog = false } + if (showChooseRatingDialog) ChooseRatingDialog(listOf(vm.curItem!!)) { showChooseRatingDialog = false } var showChaptersDialog by remember { mutableStateOf(false) } - if (showChaptersDialog) ChaptersDialog(media = currentItem!!, onDismissRequest = {showChaptersDialog = false}) + if (showChaptersDialog) ChaptersDialog(media = vm.curItem!!, onDismissRequest = {showChaptersDialog = false}) val scrollState = rememberScrollState() Column(modifier = modifier.fillMaxWidth().verticalScroll(scrollState)) { @@ -540,7 +545,7 @@ class AudioPlayerFragment : Fragment() { bitRates.addAll(bSet) bitrate = bitRates[0].toInt() ytMediaSpecs.setAudioStream(locale, codec, bitrate) - resetPlayer = true + vm.resetPlayer = true } } Spacer(Modifier.weight(1f)) @@ -558,7 +563,7 @@ class AudioPlayerFragment : Fragment() { bitRates.addAll(bSet) bitrate = bitRates[0].toInt() ytMediaSpecs.setAudioStream(locale, codec, bitrate) - resetPlayer = true + vm.resetPlayer = true } } Spacer(Modifier.weight(1f)) @@ -567,38 +572,39 @@ class AudioPlayerFragment : Fragment() { Logd(TAG, "BitRate selected: ${bitRates[index]}") bitrate = bitRates[index].toInt() ytMediaSpecs.setAudioStream(locale, codec, bitrate) - resetPlayer = true + vm.resetPlayer = true } } else Text(bitrate.toString(), color = textColor) Spacer(Modifier.weight(1f)) - if (resetPlayer) IconButton(onClick = { + if (vm.resetPlayer) IconButton(onClick = { playbackService?.mPlayer?.reinit() - resetPlayer = false + vm.resetPlayer = false }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_build_24), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "Build") } } } - Text(txtvPodcastTitle, textAlign = TextAlign.Center, color = textColor, style = MaterialTheme.typography.headlineSmall, + Text(vm.txtvPodcastTitle, textAlign = TextAlign.Center, color = textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 5.dp).combinedClickable(onClick = { - if (currentItem != null) { - if (currentItem?.feedId != null) { - val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!) + if (vm.curItem != null) { + if (vm.curItem?.feedId != null) { + val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), vm.curItem!!.feedId!!) startActivity(openFeed) } } - }, onLongClick = { copyText(currentItem?.feed?.title?:"") })) + }, onLongClick = { copyText(vm.curItem?.feed?.title?:"") })) Row(modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 2.dp)) { Spacer(modifier = Modifier.weight(0.2f)) - val ratingIconRes = Rating.fromCode(rating).res + val ratingIconRes = Rating.fromCode(vm.rating).res Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp).clickable(onClick = { showChooseRatingDialog = true })) Spacer(modifier = Modifier.weight(0.4f)) - Text(episodeDate, textAlign = TextAlign.Center, color = textColor, style = MaterialTheme.typography.bodyMedium) + Text(vm.episodeDate, textAlign = TextAlign.Center, color = textColor, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.weight(0.6f)) } - Text(titleText, textAlign = TextAlign.Center, color = textColor, style = CustomTextStyles.titleCustom, modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 5.dp) - .combinedClickable(onClick = {}, onLongClick = { copyText(currentItem?.title?:"") })) + Text(vm.titleText, textAlign = TextAlign.Center, color = textColor, style = CustomTextStyles.titleCustom, modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 5.dp) + .combinedClickable(onClick = {}, onLongClick = { copyText(vm.curItem?.title?:"") })) + // fun restoreFromPreference(): Boolean { // if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false // Logd(TAG, "Restoring from preferences") @@ -628,30 +634,30 @@ class AudioPlayerFragment : Fragment() { postDelayed({ }, 50) } } - }, update = { webView -> webView.loadDataWithBaseURL("https://127.0.0.1", if (cleanedNotes.isNullOrBlank()) "No notes" else cleanedNotes!!, "text/html", "utf-8", "about:blank") }) - if (displayedChapterIndex >= 0) { + }, update = { webView -> webView.loadDataWithBaseURL("https://127.0.0.1", if (vm.cleanedNotes.isNullOrBlank()) "No notes" else vm.cleanedNotes!!, "text/html", "utf-8", "about:blank") }) + if (vm.displayedChapterIndex >= 0) { Row(modifier = Modifier.padding(start = 20.dp, end = 20.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chapter_prev), tint = textColor, contentDescription = "prev_chapter", modifier = Modifier.width(36.dp).height(36.dp).clickable(onClick = { seekToPrevChapter() })) - Text("Ch " + displayedChapterIndex.toString() + ": " + currentChapter?.title, + Text("Ch " + vm.displayedChapterIndex.toString() + ": " + vm.curChapter?.title, color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f).padding(start = 10.dp, end = 10.dp).clickable(onClick = { showChaptersDialog = true })) - if (hasNextChapter) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chapter_next), tint = textColor, contentDescription = "next_chapter", + if (vm.hasNextChapter) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chapter_next), tint = textColor, contentDescription = "next_chapter", modifier = Modifier.width(36.dp).height(36.dp).clickable(onClick = { seekToNextChapter() })) } } - AsyncImage(model = imgLocLarge, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), + AsyncImage(model = vm.imgLocLarge, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), modifier = Modifier.fillMaxWidth().padding(start = 32.dp, end = 32.dp, top = 10.dp).clickable(onClick = {})) } } fun setIsShowPlay(showPlay: Boolean) { - Logd(TAG, "setIsShowPlay: $isShowPlay $showPlay") - if (isShowPlay != showPlay) { - isShowPlay = showPlay - playButRes = when { - isVideoScreen -> if (showPlay) R.drawable.ic_play_video_white else R.drawable.ic_pause_video_white + Logd(TAG, "setIsShowPlay: ${vm.isShowPlay} $showPlay") + if (vm.isShowPlay != showPlay) { + vm.isShowPlay = showPlay + vm.playButRes = when { + vm.isVideoScreen -> if (showPlay) R.drawable.ic_play_video_white else R.drawable.ic_pause_video_white showPlay -> R.drawable.ic_play_48dp else -> R.drawable.ic_pause } @@ -659,83 +665,83 @@ class AudioPlayerFragment : Fragment() { } private fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) { val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) - curPlaybackSpeed = event.newSpeed - txtvPlaybackSpeed = speedStr + vm.curPlaybackSpeed = event.newSpeed + vm.txtvPlaybackSpeed = speedStr } private fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) { // Logd(TAG, "onPositionUpdate") - if (!playButInit && playButRes == R.drawable.ic_play_48dp && curEpisode != null) { - if (isCurrentlyPlaying(curEpisode)) playButRes = R.drawable.ic_pause - playButInit = true + if (!vm.playButInit && vm.playButRes == R.drawable.ic_play_48dp && curEpisode != null) { + if (isCurrentlyPlaying(curEpisode)) vm.playButRes = R.drawable.ic_pause + vm.playButInit = true } - if (curEpisode?.id != event.episode?.id || controller == null || curPositionFB == Episode.INVALID_TIME || curDurationFB == Episode.INVALID_TIME) return + if (curEpisode?.id != event.episode?.id || vm.controller == null || curPositionFB == Episode.INVALID_TIME || curDurationFB == Episode.INVALID_TIME) return // val converter = TimeSpeedConverter(curSpeedFB) - currentPosition = convertOnSpeed(event.position, curSpeedFB) - duration = convertOnSpeed(event.duration, curSpeedFB) + vm.curPosition = convertOnSpeed(event.position, curSpeedFB) + vm.duration = convertOnSpeed(event.duration, curSpeedFB) val remainingTime: Int = convertOnSpeed(max((event.duration - event.position).toDouble(), 0.0).toInt(), curSpeedFB) - if (currentPosition == Episode.INVALID_TIME || duration == Episode.INVALID_TIME) { + if (vm.curPosition == Episode.INVALID_TIME || vm.duration == Episode.INVALID_TIME) { Log.w(TAG, "Could not react to position observer update because of invalid time") return } - showTimeLeft = getPref(AppPrefs.showTimeLeft, false) - txtvLengtTexth = if (showTimeLeft) (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) - else DurationConverter.getDurationStringLong(duration) - sliderValue = event.position.toFloat() + vm.showTimeLeft = getPref(AppPrefs.showTimeLeft, false) + vm.txtvLengtTexth = if (vm.showTimeLeft) (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) + else DurationConverter.getDurationStringLong(vm.duration) + vm.sliderValue = event.position.toFloat() } private fun updateUi(media: Episode) { Logd(TAG, "updateUi called $media") - titleText = media.getEpisodeTitle() - txtvPlaybackSpeed = DecimalFormat("0.00").format(curSpeedFB.toDouble()) - curPlaybackSpeed = curSpeedFB + vm.titleText = media.getEpisodeTitle() + vm.txtvPlaybackSpeed = DecimalFormat("0.00").format(curSpeedFB.toDouble()) + vm.curPlaybackSpeed = curSpeedFB onPositionUpdate(FlowEvent.PlaybackPositionEvent(media, media.position, media.duration)) if (isPlayingVideoLocally && curEpisode?.feed?.videoModePolicy != VideoMode.AUDIO_ONLY) { (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED } - prevItem = media + vm.prevItem = media } private fun updateDetails() { lifecycleScope.launch { Logd(TAG, "in updateDetails") withContext(Dispatchers.IO) { - currentItem = curEpisode - if (currentItem != null) { + vm.curItem = curEpisode + if (vm.curItem != null) { // currentItem = currentItem // currentItem = currentItem!!.episodeOrFetch() - showHomeText = false - homeText = null + vm.showHomeText = false + vm.homeText = null } - if (currentItem != null) { - if (rating == Rating.UNRATED.code || prevItem?.identifyingValue != currentItem!!.identifyingValue) rating = currentItem!!.rating + if (vm.curItem != null) { + if (vm.rating == Rating.UNRATED.code || vm.prevItem?.identifyingValue != vm.curItem!!.identifyingValue) vm.rating = vm.curItem!!.rating // currentItem = currentItem - Logd(TAG, "updateDetails updateInfo ${cleanedNotes == null} ${prevItem?.identifyingValue} ${currentItem!!.identifyingValue}") - val url = currentItem!!.downloadUrl - if (url?.contains("youtube.com") == true && currentItem!!.description?.startsWith("Short:") == true) { - Logd(TAG, "getting extended description: ${currentItem!!.title}") + Logd(TAG, "updateDetails updateInfo ${vm.cleanedNotes == null} ${vm.prevItem?.identifyingValue} ${vm.curItem!!.identifyingValue}") + val url = vm.curItem!!.downloadUrl + if (url?.contains("youtube.com") == true && vm.curItem!!.description?.startsWith("Short:") == true) { + Logd(TAG, "getting extended description: ${vm.curItem!!.title}") try { - val info = currentItem!!.streamInfo + val info = vm.curItem!!.streamInfo if (info?.description?.content != null) { - currentItem = upsert(currentItem!!) { it.description = info.description?.content } - cleanedNotes = shownotesCleaner?.processShownotes(info.description!!.content, currentItem?.duration?:0) - } else cleanedNotes = shownotesCleaner?.processShownotes(currentItem!!.description ?: "", currentItem?.duration?:0) + vm.curItem = upsert(vm.curItem!!) { it.description = info.description?.content } + vm.cleanedNotes = vm.shownotesCleaner?.processShownotes(info.description!!.content, vm.curItem?.duration?:0) + } else vm.cleanedNotes = vm.shownotesCleaner?.processShownotes(vm.curItem!!.description ?: "", vm.curItem?.duration?:0) } catch (e: Exception) { Logd(TAG, "StreamInfo error: ${e.message}") } - } else cleanedNotes = shownotesCleaner?.processShownotes(currentItem!!.description ?: "", currentItem?.duration?:0) - prevItem = currentItem + } else vm.cleanedNotes = vm.shownotesCleaner?.processShownotes(vm.curItem!!.description ?: "", vm.curItem?.duration?:0) + vm.prevItem = vm.curItem } - Logd(TAG, "updateDetails cleanedNotes: ${cleanedNotes?.length}") + Logd(TAG, "updateDetails cleanedNotes: ${vm.cleanedNotes?.length}") } withContext(Dispatchers.Main) { - Logd(TAG, "subscribe: ${currentItem?.getEpisodeTitle()}") - if (currentItem != null) { - val media = currentItem!! - Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}") + Logd(TAG, "subscribe: ${vm.curItem?.getEpisodeTitle()}") + if (vm.curItem != null) { + val media = vm.curItem!! + Logd(TAG, "displayMediaInfo ${vm.curItem?.title} ${media.getEpisodeTitle()}") val pubDateStr = MiscFormatter.formatDateTimeFlex(media.getPubDate()) - txtvPodcastTitle = (media.feed?.title?:"").trim() - episodeDate = pubDateStr.trim() - titleText = currentItem?.title ?:"" - displayedChapterIndex = -1 + vm.txtvPodcastTitle = (media.feed?.title?:"").trim() + vm.episodeDate = pubDateStr.trim() + vm.titleText = vm.curItem?.title ?:"" + vm.displayedChapterIndex = -1 refreshChapterData(media.getCurrentChapterIndex(media.position)) } Logd(TAG, "Webview loaded") @@ -744,79 +750,79 @@ class AudioPlayerFragment : Fragment() { } private fun buildHomeReaderText() { - showHomeText = !showHomeText + vm.showHomeText = !vm.showHomeText runOnIOScope { - if (showHomeText) { - homeText = currentItem!!.transcript - if (homeText == null && currentItem?.link != null) { - val url = currentItem!!.link!! + if (vm.showHomeText) { + vm.homeText = vm.curItem!!.transcript + if (vm.homeText == null && vm.curItem?.link != null) { + val url = vm.curItem!!.link!! val htmlSource = fetchHtmlSource(url) - val readability4J = Readability4J(currentItem!!.link!!, htmlSource) + val readability4J = Readability4J(vm.curItem!!.link!!, htmlSource) val article = readability4J.parse() - readerhtml = article.contentWithDocumentsCharsetOrUtf8 - if (!readerhtml.isNullOrEmpty()) { - currentItem = upsertBlk(currentItem!!) { it.setTranscriptIfLonger(readerhtml) } - homeText = currentItem!!.transcript + vm.readerhtml = article.contentWithDocumentsCharsetOrUtf8 + if (!vm.readerhtml.isNullOrEmpty()) { + vm.curItem = upsertBlk(vm.curItem!!) { it.setTranscriptIfLonger(vm.readerhtml) } + vm.homeText = vm.curItem!!.transcript } } - if (!homeText.isNullOrEmpty()) cleanedNotes = shownotesCleaner?.processShownotes(homeText!!, 0) + if (!vm.homeText.isNullOrEmpty()) vm.cleanedNotes = vm.shownotesCleaner?.processShownotes(vm.homeText!!, 0) else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() } } else { - cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", currentItem?.duration ?: 0) - if (cleanedNotes.isNullOrEmpty()) withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() } + vm.cleanedNotes = vm.shownotesCleaner?.processShownotes(vm.curItem?.description ?: "", vm.curItem?.duration ?: 0) + if (vm.cleanedNotes.isNullOrEmpty()) withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() } } } } private fun refreshChapterData(chapterIndex: Int) { Logd(TAG, "in refreshChapterData $chapterIndex") - if (currentItem != null && chapterIndex > -1) { - if (currentItem!!.position > currentItem!!.duration || chapterIndex >= (currentItem?.chapters?: listOf()).size - 1) { - displayedChapterIndex = currentItem!!.chapters.size - 1 - hasNextChapter = false + if (vm.curItem != null && chapterIndex > -1) { + if (vm.curItem!!.position > vm.curItem!!.duration || chapterIndex >= (vm.curItem?.chapters?: listOf()).size - 1) { + vm.displayedChapterIndex = vm.curItem!!.chapters.size - 1 + vm.hasNextChapter = false } else { - displayedChapterIndex = chapterIndex - hasNextChapter = true + vm.displayedChapterIndex = chapterIndex + vm.hasNextChapter = true } } - if (currentItem != null) { - imgLocLarge = if (displayedChapterIndex == -1 || currentItem?.chapters.isNullOrEmpty() || currentItem!!.chapters[displayedChapterIndex].imageUrl.isNullOrEmpty()) - currentItem!!.imageLocation else EmbeddedChapterImage.getModelFor(currentItem!!, displayedChapterIndex)?.toString() - Logd(TAG, "displayCoverImage: imgLoc: $imgLoc") + if (vm.curItem != null) { + vm.imgLocLarge = if (vm.displayedChapterIndex == -1 || vm.curItem?.chapters.isNullOrEmpty() || vm.curItem!!.chapters[vm.displayedChapterIndex].imageUrl.isNullOrEmpty()) + vm.curItem!!.imageLocation else EmbeddedChapterImage.getModelFor(vm.curItem!!, vm.displayedChapterIndex)?.toString() + Logd(TAG, "displayCoverImage: imgLoc: ${vm.imgLoc}") } } private fun seekToPrevChapter() { - val curr: Chapter? = currentChapter - if (curr == null || displayedChapterIndex == -1) return + val curr: Chapter? = vm.curChapter + if (curr == null || vm.displayedChapterIndex == -1) return when { - displayedChapterIndex < 1 -> seekTo(0) + vm.displayedChapterIndex < 1 -> seekTo(0) (curPositionFB - 10000 * curSpeedFB) < curr.start -> { - refreshChapterData(displayedChapterIndex - 1) - if (!currentItem?.chapters.isNullOrEmpty()) seekTo(currentItem!!.chapters[displayedChapterIndex].start.toInt()) + refreshChapterData(vm.displayedChapterIndex - 1) + if (!vm.curItem?.chapters.isNullOrEmpty()) seekTo(vm.curItem!!.chapters[vm.displayedChapterIndex].start.toInt()) } else -> seekTo(curr.start.toInt()) } } private fun seekToNextChapter() { - if (currentItem?.chapters.isNullOrEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= currentItem!!.chapters.size) return - refreshChapterData(displayedChapterIndex + 1) - seekTo(currentItem!!.chapters[displayedChapterIndex].start.toInt()) + if (vm.curItem?.chapters.isNullOrEmpty() || vm.displayedChapterIndex == -1 || vm.displayedChapterIndex + 1 >= vm.curItem!!.chapters.size) return + refreshChapterData(vm.displayedChapterIndex + 1) + seekTo(vm.curItem!!.chapters[vm.displayedChapterIndex].start.toInt()) } fun onExpanded() { Logd(TAG, "onExpanded()") // the function can also be called from MainActivity when a select menu pops up and closes - isCollapsed = false - if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext()) - setIsShowPlay(isShowPlay) + vm.isCollapsed = false + if (vm.shownotesCleaner == null) vm.shownotesCleaner = ShownotesCleaner(requireContext()) + setIsShowPlay(vm.isShowPlay) } fun onCollaped() { Logd(TAG, "onCollaped()") - isCollapsed = true - setIsShowPlay(isShowPlay) + vm.isCollapsed = true + setIsShowPlay(vm.isShowPlay) } private var loadItemsRunning = false @@ -830,26 +836,26 @@ class AudioPlayerFragment : Fragment() { if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true) if (!loadItemsRunning) { loadItemsRunning = true - val curMediaChanged = currentItem == null || curEpisode?.id != currentItem?.id - if (curEpisode != null && curEpisode?.id != currentItem?.id) { + val curMediaChanged = vm.curItem == null || curEpisode?.id != vm.curItem?.id + if (curEpisode != null && curEpisode?.id != vm.curItem?.id) { updateUi(curEpisode!!) // imgLoc = ImageResourceUtils.getEpisodeListImageLocation(curMedia!!) - currentItem = curEpisode + vm.curItem = curEpisode } - if (!isCollapsed && curMediaChanged) { + if (!vm.isCollapsed && curMediaChanged) { Logd(TAG, "loadMediaInfo loading details ${curEpisode?.id}") lifecycleScope.launch { withContext(Dispatchers.IO) { curEpisode?.apply { this.loadChapters(requireContext(), false) } } - currentItem = curEpisode - val item = currentItem + vm.curItem = curEpisode + val item = vm.curItem // val item = currentItem?.episodeOrFetch() if (item != null) setItem(item) - val chapters: List = currentItem?.chapters ?: listOf() + val chapters: List = vm.curItem?.chapters ?: listOf() if (chapters.isNotEmpty()) { val dividerPos = FloatArray(chapters.size) for (i in chapters.indices) dividerPos[i] = chapters[i].start / curDurationFB.toFloat() } - sleepTimerActive = isSleepTimerActive() + vm.sleepTimerActive = isSleepTimerActive() // TODO: disable for now // if (!includingChapters) loadMediaInfo(true) }.invokeOnCompletion { throwable -> @@ -862,31 +868,31 @@ class AudioPlayerFragment : Fragment() { private fun setItem(item_: Episode) { Logd(TAG, "setItem ${item_.title}") - if (currentItem?.identifyingValue != item_.identifyingValue) { - currentItem = item_ - rating = currentItem!!.rating - showHomeText = false - homeText = null + if (vm.curItem?.identifyingValue != item_.identifyingValue) { + vm.curItem = item_ + vm.rating = vm.curItem!!.rating + vm.showHomeText = false + vm.homeText = null } } override fun onResume() { - Logd(TAG, "onResume() isCollapsed: $isCollapsed") + Logd(TAG, "onResume() isCollapsed: ${vm.isCollapsed}") super.onResume() loadMediaInfo() if (curEpisode != null) onPositionUpdate(FlowEvent.PlaybackPositionEvent(curEpisode!!, curEpisode!!.position, curEpisode!!.duration)) } override fun onStart() { - Logd(TAG, "onStart() isCollapsed: $isCollapsed") + Logd(TAG, "onStart() isCollapsed: ${vm.isCollapsed}") super.onStart() procFlowEvents() val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java)) - if (controllerFuture == null) { - controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync() - controllerFuture?.addListener({ - media3Controller = controllerFuture!!.get() + if (vm.controllerFuture == null) { + vm.controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync() + vm.controllerFuture?.addListener({ + media3Controller = vm.controllerFuture!!.get() // Logd(TAG, "controllerFuture.addListener: $mediaController") }, MoreExecutors.directExecutor()) } @@ -901,14 +907,15 @@ class AudioPlayerFragment : Fragment() { override fun onPause() { super.onPause() - val editor = prefs.edit() ?: return - if (curEpisode != null) editor.putString(PREF_PLAYABLE_ID, curEpisode!!.id.toString()) - else { - Logd(TAG, "savePreferences was called while media or webview was null") - editor.putInt(PREF_SCROLL_Y, -1) - editor.putString(PREF_PLAYABLE_ID, "") - } - editor.apply() + // TODO: do we need this at all? +// val editor = prefs.edit() ?: return +// if (curEpisode != null) editor.putString(PREF_PLAYABLE_ID, curEpisode!!.id.toString()) +// else { +// Logd(TAG, "savePreferences was called while media or webview was null") +// editor.putInt(PREF_SCROLL_Y, -1) +// editor.putString(PREF_PLAYABLE_ID, "") +// } +// editor.apply() } // @Subscribe(threadMode = ThreadMode.MAIN) @@ -933,9 +940,9 @@ class AudioPlayerFragment : Fragment() { private fun onPlayEvent(event: FlowEvent.PlayEvent) { Logd(TAG, "onPlayEvent ${event.episode.title}") val currentitem = event.episode - if (currentItem?.id == null || currentitem.id != currentItem?.id) { - currentItem = currentitem - updateUi(currentItem!!) + if (vm.curItem?.id == null || currentitem.id != vm.curItem?.id) { + vm.curItem = currentitem + updateUi(vm.curItem!!) setItem(currentitem) } (activity as MainActivity).setPlayerVisible(true) @@ -945,17 +952,17 @@ class AudioPlayerFragment : Fragment() { private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { // Logd(TAG, "onPlaybackPositionEvent ${event.episode.title}") val media = event.episode ?: return - if (currentItem?.id == null || media.id != currentItem?.id) { - currentItem = media - updateUi(currentItem!!) + if (vm.curItem?.id == null || media.id != vm.curItem?.id) { + vm.curItem = media + updateUi(vm.curItem!!) setItem(curEpisode!!) } // if (isShowPlay) setIsShowPlay(false) onPositionUpdate(event) - if (!isCollapsed) { - if (currentItem?.id != event.episode.id) return - val newChapterIndex: Int = currentItem!!.getCurrentChapterIndex(event.position) - if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex) + if (!vm.isCollapsed) { + if (vm.curItem?.id != event.episode.id) return + val newChapterIndex: Int = vm.curItem!!.getCurrentChapterIndex(event.position) + if (newChapterIndex >= 0 && newChapterIndex != vm.displayedChapterIndex) refreshChapterData(newChapterIndex) } } @@ -981,13 +988,13 @@ class AudioPlayerFragment : Fragment() { } } is FlowEvent.PlayEvent -> onPlayEvent(event) - is FlowEvent.RatingEvent -> if (curEpisode?.id == event.episode.id) rating = event.rating + is FlowEvent.RatingEvent -> if (curEpisode?.id == event.episode.id) vm.rating = event.rating // is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false) is FlowEvent.PlayerErrorEvent -> { - showErrorDialog.value = true - errorMessage = event.message + vm.showErrorDialog.value = true + vm.errorMessage = event.message } - is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) sleepTimerActive = isSleepTimerActive() + is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) vm.sleepTimerActive = isSleepTimerActive() is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event) is FlowEvent.SpeedChangedEvent -> updatePlaybackSpeedButton(event) else -> {} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt index cbf254b6..e8190219 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt @@ -2,25 +2,25 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.SelectCountryDialogBinding import ac.mdiq.podcini.net.feed.searcher.ItunesTopListLoader import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.compose.CustomTextStyles import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.OnlineFeedItem import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.Logd import android.content.Context -import android.content.DialogInterface import android.content.SharedPreferences import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ArrayAdapter +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -36,16 +36,17 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.MaterialAutoCompleteTextView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.* class DiscoveryFragment : Fragment() { - val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) } + internal val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) } class DiscoveryVM { internal var topList: List? = listOf() @@ -59,68 +60,92 @@ class DiscoveryFragment : Fragment() { internal var retryQerry by mutableStateOf("") internal var showProgress by mutableStateOf(true) internal var noResultText by mutableStateOf("") - } - - private val vm = DiscoveryVM() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - vm.countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country) - vm.hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false) - vm.needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) + internal var showSelectCounrty by mutableStateOf(false) } + private lateinit var vm: DiscoveryVM + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - val textColor = MaterialTheme.colorScheme.onSurface - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - ConstraintLayout(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - val (gridView, progressBar, empty, txtvError, butRetry) = createRefs() - if (vm.showProgress) CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) }) - val lazyListState = rememberLazyListState() - if (vm.searchResults.isNotEmpty()) LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) - .constrainAs(gridView) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - }, - verticalArrangement = Arrangement.spacedBy(8.dp)) { - items(vm.searchResults.size) { index -> OnlineFeedItem(activity = activity as MainActivity, vm.searchResults[index]) } - } - if (vm.searchResults.isEmpty()) Text(vm.noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) - if (vm.errorText.isNotEmpty()) Text(vm.errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) }) - if (vm.retryQerry.isNotEmpty()) Button( - modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom) }, - onClick = { - if (vm.needsConfirm) { - prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply() - vm.needsConfirm = false - } - loadToplist(vm.countryCode) - }, - ) { Text(stringResource(id = R.string.retry_label)) } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { DiscoveryScreen() } } } + return composeView + } + + @Composable + fun DiscoveryScreen() { + if (!::vm.isInitialized) vm = remember { DiscoveryVM() } + + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_CREATE -> { + Logd(TAG, "ON_CREATE") + vm.countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country) + vm.hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false) + vm.needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) + loadToplist(vm.countryCode) + } + Lifecycle.Event.ON_START -> { + Logd(TAG, "ON_START") + } + Lifecycle.Event.ON_STOP -> { + Logd(TAG, "ON_STOP") + } + Lifecycle.Event.ON_DESTROY -> { + Logd(TAG, "ON_DESTROY") + vm.searchResults.clear() + vm.topList = null + } + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + val textColor = MaterialTheme.colorScheme.onSurface + + if (vm.showSelectCounrty == true) SelectCountryDialog { vm.showSelectCounrty = false } + + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + ConstraintLayout(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + val (gridView, progressBar, empty, txtvError, butRetry) = createRefs() + if (vm.showProgress) CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) }) + val lazyListState = rememberLazyListState() + if (vm.searchResults.isNotEmpty()) LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) + .constrainAs(gridView) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }, + verticalArrangement = Arrangement.spacedBy(8.dp)) { + items(vm.searchResults.size) { index -> OnlineFeedItem(activity = activity as MainActivity, vm.searchResults[index]) } + } + if (vm.searchResults.isEmpty()) Text(vm.noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) + if (vm.errorText.isNotEmpty()) Text(vm.errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) }) + if (vm.retryQerry.isNotEmpty()) Button( + modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom) }, + onClick = { + if (vm.needsConfirm) { + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply() + vm.needsConfirm = false + } + loadToplist(vm.countryCode) + }, + ) { Text(stringResource(id = R.string.retry_label)) } // Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background( // Color.LightGray) // .constrainAs(powered) { // bottom.linkTo(parent.bottom) // end.linkTo(parent.end) // }) - } - } - } } } - loadToplist(vm.countryCode) - return composeView - } - - override fun onDestroy() { - vm.searchResults.clear() - vm.topList = null - super.onDestroy() } private fun loadToplist(country: String?) { @@ -189,7 +214,7 @@ class DiscoveryFragment : Fragment() { expanded = false }) DropdownMenuItem(text = { Text(stringResource(R.string.select_country)) }, onClick = { - selectCountry() + vm.showSelectCounrty = true expanded = false }) } @@ -197,59 +222,74 @@ class DiscoveryFragment : Fragment() { ) } - private fun selectCountry() { - val inflater = layoutInflater - val selectCountryDialogView = inflater.inflate(R.layout.select_country_dialog, null) - val builder = MaterialAlertDialogBuilder(requireContext()) - builder.setView(selectCountryDialogView) - - val countryCodeArray: List = listOf(*Locale.getISOCountries()) - val countryCodeNames: MutableMap = HashMap() - val countryNameCodes: MutableMap = HashMap() - for (code in countryCodeArray) { - val locale = Locale("", code) - val countryName = locale.displayCountry - countryCodeNames[code] = countryName - countryNameCodes[countryName] = code - } - - val countryNamesSort: MutableList = ArrayList(countryCodeNames.values) - countryNamesSort.sort() + @Composable + fun SelectCountryDialog(onDismiss: () -> Unit) { + val countryNameCodeMap: MutableMap = remember { hashMapOf() } + val countryCodeNameMap: MutableMap = remember { hashMapOf() } + val countryNamesSort = remember { mutableStateListOf() } + var selectedCountry by remember { mutableStateOf("") } + var textInput by remember { mutableStateOf("") } - val dataAdapter = ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, countryNamesSort) - val scBinding = SelectCountryDialogBinding.bind(selectCountryDialogView) - val textInput = scBinding.countryTextInput - val editText = textInput.editText as? MaterialAutoCompleteTextView - editText!!.setAdapter(dataAdapter) - editText.setText(countryCodeNames[vm.countryCode]) - editText.setOnClickListener { - if (editText.text.isNotEmpty()) { - editText.setText("") - editText.postDelayed({ editText.showDropDown() }, 100) - } - } - editText.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean -> - if (hasFocus) { - editText.setText("") - editText.postDelayed({ editText.showDropDown() }, 100) + LaunchedEffect(Unit) { + val countryCodeArray: List = listOf(*Locale.getISOCountries()) + for (code in countryCodeArray) { + val locale = Locale("", code) + val countryName = locale.displayCountry + Logd(TAG, "code: $code countryName: $countryName") + countryCodeNameMap[code] = countryName + countryNameCodeMap[countryName] = code } + countryNamesSort.addAll(countryCodeNameMap.values) + countryNamesSort.sort() + selectedCountry = countryCodeNameMap[vm.countryCode] ?: "" + textInput = selectedCountry } - - builder.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - val countryName = editText.text.toString() - if (countryNameCodes.containsKey(countryName)) { - vm.countryCode = countryNameCodes[countryName] - vm.hidden = false + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun CountrySelection() { + val filteredCountries = remember { countryNamesSort.toMutableStateList() } + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) { + TextField(value = textInput, modifier = Modifier.fillMaxWidth().padding(20.dp).menuAnchor(MenuAnchorType.PrimaryNotEditable, false), readOnly = false, + onValueChange = { input -> + textInput = input + if (textInput.length > 1) { + filteredCountries.clear() + filteredCountries.addAll(countryNamesSort.filter { it.contains(input, ignoreCase = true) }.take(5)) + Logd(TAG, "input: $input filteredCountries: ${filteredCountries.size}") + expanded = filteredCountries.isNotEmpty() + } + }, + label = { Text(stringResource(id = R.string.select_country)) }) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + filteredCountries.forEach { country -> + DropdownMenuItem(text = { Text(text = country) }, onClick = { + selectedCountry = country + textInput = country + expanded = false + }) + } + } } - - prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, vm.hidden).apply() - prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, vm.countryCode).apply() - - EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent()) - loadToplist(vm.countryCode) } - builder.setNegativeButton(R.string.cancel_label, null) - builder.show() + AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = { onDismiss() }, + title = { Text(stringResource(R.string.pref_custom_media_dir_title), style = CustomTextStyles.titleCustom) }, + text = { CountrySelection() }, + confirmButton = { + TextButton(onClick = { + if (countryNameCodeMap.containsKey(selectedCountry)) { + vm.countryCode = countryNameCodeMap[selectedCountry] + vm.hidden = false + } + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, vm.hidden).apply() + prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, vm.countryCode).apply() + EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent()) + loadToplist(vm.countryCode) + onDismiss() + }) { Text(stringResource(R.string.confirm_label)) } + }, + dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.cancel_label)) } } + ) } companion object { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt index 86be6cdf..cd20ec6e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt @@ -77,7 +77,7 @@ class EpisodeHomeFragment : Fragment() { super.onCreateView(inflater, container, savedInstanceState) Logd(TAG, "fragment onCreateView") - val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { HomeView() } } } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { EpisodeHomeView() } } } if (!episode?.link.isNullOrEmpty()) prepareContent() else { @@ -88,7 +88,7 @@ class EpisodeHomeFragment : Fragment() { } @Composable - fun HomeView() { + fun EpisodeHomeView() { fun Color.toHex(): String { val red = (red * 255).toInt().toString(16).padStart(2, '0') val green = (green * 255).toInt().toString(16).padStart(2, '0') diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index d3d93ec9..5351baef 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -112,7 +112,7 @@ class EpisodeInfoFragment : Fragment() { super.onCreateView(inflater, container, savedInstanceState) Logd(TAG, "fragment onCreateView") - val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { EpisodeInfoScreen() } } } vm.shownotesCleaner = ShownotesCleaner(requireContext()) updateAppearance() load() @@ -120,7 +120,7 @@ class EpisodeInfoFragment : Fragment() { } @Composable - fun MainView() { + fun EpisodeInfoScreen() { val textColor = MaterialTheme.colorScheme.onSurface var showEditComment by remember { mutableStateOf(false) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt index 18dfca85..a9fa25df 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt @@ -124,30 +124,8 @@ class EpisodesFragment : Fragment() { vm.rightActionState.value = vm.swipeActions.actions.right[0] lifecycle.addObserver(vm.swipeActions) - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - OpenDialog() - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) - EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, - buildMoreItems = { buildMoreItems() }, - leftSwipeCB = { - if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.leftActionState.value.performAction(it) - }, - rightSwipeCB = { - if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.rightActionState.value.performAction(it) - }, - actionButton_ = vm.actionButtonToPass - ) - } - } - } - } - } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { EpisodesScreen() } } } + refreshSwipeTelltale() vm.curIndex = prefs.getInt("curIndex", 0) vm.sortOrder = vm.episodesSortOrder @@ -155,6 +133,28 @@ class EpisodesFragment : Fragment() { return composeView } + @Composable + fun EpisodesScreen() { + OpenDialog() + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, + buildMoreItems = { buildMoreItems() }, + leftSwipeCB = { + if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionState.value.performAction(it) + }, + rightSwipeCB = { + if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionState.value.performAction(it) + }, + actionButton_ = vm.actionButtonToPass + ) + } + } + } + override fun onStart() { super.onStart() procFlowEvents() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index ce13bd3b..91bf2237 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -81,7 +81,7 @@ class FeedEpisodesFragment : Fragment() { internal var headerCreated = false internal var feedID: Long = 0 internal var feed by mutableStateOf(null) - var rating by mutableStateOf(Rating.UNRATED.code) + internal var rating by mutableStateOf(Rating.UNRATED.code) internal val episodes = mutableStateListOf() internal val vms = mutableStateListOf() @@ -95,11 +95,25 @@ class FeedEpisodesFragment : Fragment() { internal var showRemoveFeedDialog by mutableStateOf(false) internal var showFilterDialog by mutableStateOf(false) internal var showRenameDialog by mutableStateOf(false) - var showSortDialog by mutableStateOf(false) - var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD) - var layoutModeIndex by mutableIntStateOf(0) + internal var showSortDialog by mutableStateOf(false) + internal var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD) + internal var layoutModeIndex by mutableIntStateOf(0) internal var onInit: Boolean = true + internal var filterJob: Job? = null + + internal var eventSink: Job? = null + internal var eventStickySink: Job? = null + internal var eventKeySink: Job? = null + internal fun cancelFlowEvents() { + eventSink?.cancel() + eventSink = null + eventStickySink?.cancel() + eventStickySink = null + eventKeySink?.cancel() + eventKeySink = null + } + } private val vm = FeedEpisodesVM() @@ -122,108 +136,106 @@ class FeedEpisodesFragment : Fragment() { vm.rightActionState.value = vm.swipeActions.actions.right[0] lifecycle.addObserver(vm.swipeActions) - var filterJob: Job? = null - fun filterLongClick() { - if (vm.feed == null) return - vm.enableFilter = !vm.enableFilter - if (filterJob != null) { - filterJob?.cancel() + vm.layoutModeIndex = if (vm.feed?.useWideLayout == true) 1 else 0 + + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { FeedEpisodesScreen() } } } + + lifecycle.addObserver(vm.swipeActions) + refreshSwipeTelltale() + return composeView + } + + fun filterLongClick() { + if (vm.feed == null) return + vm.enableFilter = !vm.enableFilter + if (vm.filterJob != null) { + Logd(TAG, "filterLongClick") + vm.filterJob?.cancel() + stopMonitor(vm.vms) + vm.vms.clear() + } + vm.filterJob = lifecycleScope.launch { + val eListTmp = mutableListOf() + withContext(Dispatchers.IO) { + if (vm.enableFilter) { + vm.filterButtonColor.value = Color.White + val episodes_ = realm.query(Episode::class).query("feedId == ${vm.feed!!.id}").query(vm.feed!!.episodeFilter.queryString()).find() + eListTmp.addAll(episodes_) + } else { + vm.filterButtonColor.value = Color.Red + eListTmp.addAll(vm.feed!!.episodes) + } + getPermutor(fromCode(vm.feed?.sortOrderCode ?: 0)).reorder(eListTmp) + vm.episodes.clear() + vm.episodes.addAll(eListTmp) + vm.ieMap = vm.episodes.withIndex().associate { (index, episode) -> episode.id to index } + vm.ueMap = vm.episodes.mapIndexedNotNull { index, episode -> episode.downloadUrl?.let { it to index } }.toMap() + } + withContext(Dispatchers.Main) { stopMonitor(vm.vms) vm.vms.clear() + for (e in eListTmp) vm.vms.add(EpisodeVM(e, TAG)) } - filterJob = lifecycleScope.launch { - val eListTmp = mutableListOf() - withContext(Dispatchers.IO) { - if (vm.enableFilter) { - vm.filterButtonColor.value = Color.White - val episodes_ = realm.query(Episode::class).query("feedId == ${vm.feed!!.id}").query(vm.feed!!.episodeFilter.queryString()).find() - eListTmp.addAll(episodes_) - } else { - vm.filterButtonColor.value = Color.Red - eListTmp.addAll(vm.feed!!.episodes) - } - getPermutor(fromCode(vm.feed?.sortOrderCode ?: 0)).reorder(eListTmp) - vm.episodes.clear() - vm.episodes.addAll(eListTmp) - vm.ieMap = vm.episodes.withIndex().associate { (index, episode) -> episode.id to index } - vm.ueMap = vm.episodes.mapIndexedNotNull { index, episode -> episode.downloadUrl?.let { it to index } }.toMap() - } - withContext(Dispatchers.Main) { - stopMonitor(vm.vms) - vm.vms.clear() - for (e in eListTmp) vm.vms.add(EpisodeVM(e, TAG)) - } - }.apply { invokeOnCompletion { filterJob = null } } - } - - vm.layoutModeIndex = if (vm.feed?.useWideLayout == true) 1 else 0 + }.apply { invokeOnCompletion { vm.filterJob = null } } + } - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (vm.showRemoveFeedDialog) RemoveFeedDialog(listOf(vm.feed!!), onDismissRequest = { vm.showRemoveFeedDialog = false }) { - (activity as MainActivity).loadFragment(AppPreferences.defaultPage, null) - // Make sure fragment is hidden before actually starting to delete - requireActivity().supportFragmentManager.executePendingTransactions() - } - if (vm.showFilterDialog) EpisodesFilterDialog(filter = vm.feed!!.episodeFilter, -// filtersDisabled = mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA), - onDismissRequest = { vm.showFilterDialog = false }) { filterValues -> - if (vm.feed != null) { - Logd(TAG, "persist Episode Filter(): feedId = [${vm.feed?.id}], filterValues = [$filterValues]") - runOnIOScope { - val feed_ = realm.query(Feed::class, "id == ${vm.feed!!.id}").first().find() - if (feed_ != null) { - vm.feed = upsert(feed_) { it.filterString = filterValues.joinToString() } + @Composable + fun FeedEpisodesScreen() { + if (vm.showRemoveFeedDialog) RemoveFeedDialog(listOf(vm.feed!!), onDismissRequest = { vm.showRemoveFeedDialog = false }) { + (activity as MainActivity).loadFragment(AppPreferences.defaultPage, null) + // Make sure fragment is hidden before actually starting to delete + requireActivity().supportFragmentManager.executePendingTransactions() + } + if (vm.showFilterDialog) EpisodesFilterDialog(filter = vm.feed!!.episodeFilter, + onDismissRequest = { vm.showFilterDialog = false }) { filterValues -> + if (vm.feed != null) { + Logd(TAG, "persist Episode Filter(): feedId = [${vm.feed?.id}], filterValues = [$filterValues]") + runOnIOScope { + val feed_ = realm.query(Feed::class, "id == ${vm.feed!!.id}").first().find() + if (feed_ != null) { + vm.feed = upsert(feed_) { it.filterString = filterValues.joinToString() } // loadFeed() - } - } - } - } - if (vm.showRenameDialog) RenameOrCreateSyntheticFeed(vm.feed) { vm.showRenameDialog = false } - if (vm.showSortDialog) EpisodeSortDialog(initOrder = vm.sortOrder, onDismissRequest = { vm.showSortDialog = false }) { sortOrder_, _ -> - if (vm.feed != null) { - Logd(TAG, "persist Episode SortOrder_") - vm.sortOrder = sortOrder_ - runOnIOScope { - val feed_ = realm.query(Feed::class, "id == ${vm.feed!!.id}").first().find() - if (feed_ != null) vm.feed = upsert(feed_) { it.sortOrder = sortOrder_ } - } - } - } - if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> - vm.swipeActions.actions = actions - refreshSwipeTelltale() - } - vm.swipeActions.ActionOptionsDialog() - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = vm.filterButtonColor.value, filterClickCB = { - if (vm.enableFilter && vm.feed != null) vm.showFilterDialog = true - }, filterLongClickCB = { filterLongClick() }) - InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) - EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, feed = vm.feed, layoutMode = vm.layoutModeIndex, - buildMoreItems = { buildMoreItems() }, - refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), vm.feed) }, - leftSwipeCB = { - if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.leftActionState.value.performAction(it) - }, - rightSwipeCB = { - Logd(TAG, "vm.rightActionState: ${vm.rightActionState.value.getId()}") - if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.rightActionState.value.performAction(it) - }, - ) - } } } } } - - lifecycle.addObserver(vm.swipeActions) - refreshSwipeTelltale() - return composeView + if (vm.showRenameDialog) RenameOrCreateSyntheticFeed(vm.feed) { vm.showRenameDialog = false } + if (vm.showSortDialog) EpisodeSortDialog(initOrder = vm.sortOrder, onDismissRequest = { vm.showSortDialog = false }) { sortOrder_, _ -> + if (vm.feed != null) { + Logd(TAG, "persist Episode SortOrder_") + vm.sortOrder = sortOrder_ + runOnIOScope { + val feed_ = realm.query(Feed::class, "id == ${vm.feed!!.id}").first().find() + if (feed_ != null) vm.feed = upsert(feed_) { it.sortOrder = sortOrder_ } + } + } + } + if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> + vm.swipeActions.actions = actions + refreshSwipeTelltale() + } + vm.swipeActions.ActionOptionsDialog() + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = vm.filterButtonColor.value, filterClickCB = { + if (vm.enableFilter && vm.feed != null) vm.showFilterDialog = true + }, filterLongClickCB = { filterLongClick() }) + InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, feed = vm.feed, layoutMode = vm.layoutModeIndex, + buildMoreItems = { buildMoreItems() }, + refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), vm.feed) }, + leftSwipeCB = { + if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionState.value.performAction(it) + }, + rightSwipeCB = { + Logd(TAG, "vm.rightActionState: ${vm.rightActionState.value.getId()}") + if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionState.value.performAction(it) + }, + ) + } + } } override fun onStart() { @@ -236,7 +248,7 @@ class FeedEpisodesFragment : Fragment() { override fun onStop() { Logd(TAG, "onStop() called") super.onStop() - cancelFlowEvents() + vm.cancelFlowEvents() } @OptIn(ExperimentalFoundationApi::class) @@ -375,8 +387,7 @@ class FeedEpisodesFragment : Fragment() { }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.playlist_play), contentDescription = "queue") } if (vm.feed != null) IconButton(onClick = { (activity as MainActivity).loadChildFragment(SearchFragment.newInstance(vm.feed!!.id, vm.feed!!.title)) }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_search), contentDescription = "search") } - if (!vm.feed?.link.isNullOrBlank()) IconButton(onClick = { - IntentUtils.openInBrowser(requireContext(), vm.feed!!.link!!) + if (!vm.feed?.link.isNullOrBlank()) IconButton(onClick = { IntentUtils.openInBrowser(requireContext(), vm.feed!!.link!!) }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_web), contentDescription = "web") } if (vm.feed != null) { IconButton(onClick = { expanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Menu") } @@ -442,19 +453,8 @@ class FeedEpisodesFragment : Fragment() { } } - private var eventSink: Job? = null - private var eventStickySink: Job? = null - private var eventKeySink: Job? = null - private fun cancelFlowEvents() { - eventSink?.cancel() - eventSink = null - eventStickySink?.cancel() - eventStickySink = null - eventKeySink?.cancel() - eventKeySink = null - } private fun procFlowEvents() { - if (eventSink == null) eventSink = lifecycleScope.launch { + if (vm.eventSink == null) vm.eventSink = lifecycleScope.launch { EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { @@ -465,7 +465,7 @@ class FeedEpisodesFragment : Fragment() { } } } - if (eventStickySink == null) eventStickySink = lifecycleScope.launch { + if (vm.eventStickySink == null) vm.eventStickySink = lifecycleScope.launch { EventFlow.stickyEvents.collectLatest { event -> Logd(TAG, "Received sticky event: ${event.TAG}") when (event) { @@ -475,12 +475,12 @@ class FeedEpisodesFragment : Fragment() { } } } - if (eventKeySink == null) eventKeySink = lifecycleScope.launch { - EventFlow.keyEvents.collectLatest { event -> - Logd(TAG, "Received key event: $event, ignored") -// onKeyUp(event) - } - } +// if (vm.eventKeySink == null) vm.eventKeySink = lifecycleScope.launch { +// EventFlow.keyEvents.collectLatest { event -> +// Logd(TAG, "Received key event: $event, ignored") +//// onKeyUp(event) +// } +// } } private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdatingEvent) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index babca01c..0a3eefca 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -79,9 +79,15 @@ class FeedInfoFragment : Fragment() { internal var isCallable by mutableStateOf(false) internal var showRemoveFeedDialog by mutableStateOf(false) internal var txtvAuthor by mutableStateOf("") - var txtvUrl by mutableStateOf(null) - var rating by mutableStateOf(Rating.UNRATED.code) + internal var txtvUrl by mutableStateOf(null) + internal var rating by mutableStateOf(Rating.UNRATED.code) internal val showConnectLocalFolderConfirm = mutableStateOf(false) + + internal var eventSink: Job? = null + internal fun cancelFlowEvents() { + eventSink?.cancel() + eventSink = null + } } private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) } @@ -92,29 +98,26 @@ class FeedInfoFragment : Fragment() { vm.txtvAuthor = vm.feed.author ?: "" vm.txtvUrl = vm.feed.downloadUrl - vm.isCallable = IntentUtils.isCallable(requireContext(), Intent(Intent.ACTION_VIEW, Uri.parse(vm.feed.link))) + if (!vm.feed.link.isNullOrEmpty()) vm.isCallable = IntentUtils.isCallable(requireContext(), Intent(Intent.ACTION_VIEW, Uri.parse(vm.feed.link))) + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { FeedInfoScreen() } } } + return composeView + } - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (vm.showRemoveFeedDialog) RemoveFeedDialog(listOf(vm.feed), onDismissRequest = { vm.showRemoveFeedDialog = false }) { - (activity as MainActivity).loadFragment(AppPreferences.defaultPage, null) - // Make sure fragment is hidden before actually starting to delete - requireActivity().supportFragmentManager.executePendingTransactions() - } - ComfirmDialog(0, stringResource(R.string.reconnect_local_folder_warning), vm.showConnectLocalFolderConfirm) { - try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") } - } - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - HeaderUI() - DetailUI() - } - } - } + @Composable + fun FeedInfoScreen() { + if (vm.showRemoveFeedDialog) RemoveFeedDialog(listOf(vm.feed), onDismissRequest = { vm.showRemoveFeedDialog = false }) { + (activity as MainActivity).loadFragment(AppPreferences.defaultPage, null) + requireActivity().supportFragmentManager.executePendingTransactions() // Make sure fragment is hidden before actually starting to delete + } + ComfirmDialog(0, stringResource(R.string.reconnect_local_folder_warning), vm.showConnectLocalFolderConfirm) { + try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") } + } + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + HeaderUI() + DetailUI() } } - return composeView } override fun onStart() { @@ -126,7 +129,19 @@ class FeedInfoFragment : Fragment() { override fun onStop() { Logd(TAG, "onStop() called") super.onStop() - cancelFlowEvents() + vm.cancelFlowEvents() + } + + private fun procFlowEvents() { + if (vm.eventSink == null) vm.eventSink = lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + Logd(TAG, "Received event: ${event.TAG}") + when (event) { + is FlowEvent.FeedChangeEvent -> setFeed(vm.feed) // reload from DB + else -> {} + } + } + } } @Composable @@ -305,8 +320,7 @@ class FeedInfoFragment : Fragment() { actions = { if (vm.feed.link != null && vm.isCallable) IconButton(onClick = { IntentUtils.openInBrowser(requireContext(), vm.feed.link!!) }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_web), contentDescription = "web") } - if (!vm.feed.isLocalFeed) IconButton(onClick = { - ShareUtils.shareFeedLinkNew(requireContext(), vm.feed) + if (!vm.feed.isLocalFeed) IconButton(onClick = { ShareUtils.shareFeedLinkNew(requireContext(), vm.feed) }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_share), contentDescription = "web") } IconButton(onClick = { expanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Menu") } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { @@ -348,26 +362,6 @@ class FeedInfoFragment : Fragment() { } } - private var eventSink: Job? = null - private fun cancelFlowEvents() { - eventSink?.cancel() - eventSink = null - } - private fun procFlowEvents() { - if (eventSink == null) eventSink = lifecycleScope.launch { - EventFlow.events.collectLatest { event -> - Logd(TAG, "Received event: ${event.TAG}") - when (event) { - is FlowEvent.FeedChangeEvent -> { - setFeed(vm.feed) -// feed = event.feed - } - else -> {} - } - } - } - } - private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() { override fun createIntent(context: Context, input: Uri?): Intent { return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index dc7d1752..ae4f8314 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -55,20 +55,25 @@ import androidx.compose.ui.window.DialogProperties import androidx.fragment.app.Fragment class FeedSettingsFragment : Fragment() { - @set:JvmName("setFeedProperty") - private var feed by mutableStateOf(null) - private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default) - private var curPrefQueue by mutableStateOf(feed?.queueTextExt ?: "Default") - private var autoDeletePolicy = AutoDeleteAction.GLOBAL.name - private var videoModeSummaryResId by mutableIntStateOf(R.string.global_default) - private var videoMode = VideoMode.NONE.name - private var queues: List? = null + class FeedSettingsVM { + internal var feed by mutableStateOf(null) + + internal var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default) + internal var curPrefQueue by mutableStateOf(feed?.queueTextExt ?: "Default") + internal var autoDeletePolicy = AutoDeleteAction.GLOBAL.name + internal var videoModeSummaryResId by mutableIntStateOf(R.string.global_default) + internal var videoMode = VideoMode.NONE.name + internal var queues: List? = null + + internal var notificationPermissionDenied: Boolean = false + } + + private val vm = FeedSettingsVM() - private var notificationPermissionDenied: Boolean = false private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> if (isGranted) return@registerForActivityResult - if (notificationPermissionDenied) { + if (vm.notificationPermissionDenied) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", requireContext().packageName, null) intent.setData(uri) @@ -76,355 +81,352 @@ class FeedSettingsFragment : Fragment() { return@registerForActivityResult } Toast.makeText(context, R.string.notification_permission_denied, Toast.LENGTH_LONG).show() - notificationPermissionDenied = true + vm.notificationPermissionDenied = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") getVideoModePolicy() getAutoDeletePolicy() + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { FeedSettingsScreen() } } } + return composeView + } - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - val textColor = MaterialTheme.colorScheme.onSurface - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - val scrollState = rememberScrollState() - Column(modifier = Modifier.padding(innerPadding).padding(start = 20.dp, end = 16.dp).verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Column { - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.rounded_responsive_layout_24), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.use_wide_layout), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.useWideLayout == true) } - Switch(checked = checked, modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> f.useWideLayout = checked } - } - ) - } - Text(text = stringResource(R.string.use_wide_layout_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - if ((feed?.id ?: 0) > MAX_SYNTHETIC_ID) { - // refresh - Column { - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.keep_updated), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.keepUpdated == true) } - Switch(checked = checked, modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> f.keepUpdated = checked } - } - ) - } - Text(text = stringResource(R.string.keep_updated_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - Column { - var showDialog by remember { mutableStateOf(false) } - var selectedOption by remember { mutableStateOf(feed?.audioTypeSetting?.tag ?: Feed.AudioType.SPEECH.tag) } - if (showDialog) SetAudioType(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_feed_audio_type), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { - selectedOption = feed!!.audioTypeSetting.tag - showDialog = true - }) - ) - } - Text(text = stringResource(R.string.pref_feed_audio_type_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - if ((feed?.id ?: 0) >= MAX_NATURAL_SYNTHETIC_ID && feed?.hasVideoMedia == true) { - // video mode - Column { - Row(Modifier.fillMaxWidth()) { - var showDialog by remember { mutableStateOf(false) } - if (showDialog) VideoModeDialog(initMode = feed?.videoModePolicy, onDismissRequest = { showDialog = false }) { mode -> - feed = upsertBlk(feed!!) { it.videoModePolicy = mode } - getVideoModePolicy() - } - Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.feed_video_mode_label), style = CustomTextStyles.titleCustom, color = textColor, modifier = Modifier.clickable(onClick = { showDialog = true })) - } - Text(text = stringResource(videoModeSummaryResId), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - if (feed?.type != Feed.FeedType.YOUTUBE.name) { - // prefer streaming - Column { - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.ic_stream), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_stream_over_download_title), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.prefStreamOverDownload == true) } - Switch(checked = checked, modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> f.prefStreamOverDownload = checked } - } - ) - } - Text(text = stringResource(R.string.pref_stream_over_download_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - if (feed?.type == Feed.FeedType.YOUTUBE.name) { - // audio quality - Column { - var showDialog by remember { mutableStateOf(false) } - var selectedOption by remember { mutableStateOf(feed?.audioQualitySetting?.tag ?: Feed.AVQuality.GLOBAL.tag) } - if (showDialog) SetAudioQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_feed_audio_quality), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { - selectedOption = feed!!.audioQualitySetting.tag - showDialog = true - }) - ) - } - Text(text = stringResource(R.string.pref_feed_audio_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - if (feed?.videoModePolicy != VideoMode.AUDIO_ONLY) { - // video quality - Column { - var showDialog by remember { mutableStateOf(false) } - var selectedOption by remember { mutableStateOf(feed?.videoQualitySetting?.tag ?: Feed.AVQuality.GLOBAL.tag) } - if (showDialog) SetVideoQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_feed_video_quality), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { - selectedOption = feed!!.videoQualitySetting.tag - showDialog = true - }) - ) - } - Text(text = stringResource(R.string.pref_feed_video_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - } - // associated queue - Column { - curPrefQueue = feed?.queueTextExt ?: "Default" - var showDialog by remember { mutableStateOf(false) } - var selectedOption by remember { mutableStateOf(feed?.queueText ?: "Default") } - if (showDialog) SetAssociatedQueue(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_feed_associated_queue), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { - selectedOption = feed?.queueText ?: "Default" - showDialog = true - }) - ) - } - Text(text = curPrefQueue + " : " + stringResource(R.string.pref_feed_associated_queue_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - // auto add new to queue - if (curPrefQueue != "None") { - Column { - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = androidx.media3.session.R.drawable.media3_icon_queue_add), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.audo_add_new_queue), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.autoAddNewToQueue != false) } - Switch(checked = checked, modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> f.autoAddNewToQueue = checked } - } - ) - } - Text(text = stringResource(R.string.audo_add_new_queue_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - if (feed?.type != Feed.FeedType.YOUTUBE.name) { - // auto delete - Column { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AutoDeleteDialog(onDismissRequest = { showDialog.value = false }) - Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.auto_delete_label), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) - } - Text(text = stringResource(autoDeleteSummaryResId), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - } - // tags - Column { - var showDialog by remember { mutableStateOf(false) } - if (showDialog) TagSettingDialog(feeds_ = listOf(feed!!), onDismiss = { showDialog = false }) - Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.feed_tags_label), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog = true })) - } - Text(text = stringResource(R.string.feed_tags_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - // playback speed - Column { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) PlaybackSpeedDialog(listOf(feed!!), initSpeed = feed!!.playSpeed, maxSpeed = 3f, - onDismiss = { showDialog.value = false }) { newSpeed -> - feed = upsertBlk(feed!!) { it.playSpeed = newSpeed } - } - Icon(ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.playback_speed), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) - } - Text(text = stringResource(R.string.pref_feed_playback_speed_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + @Composable + fun FeedSettingsScreen() { + val textColor = MaterialTheme.colorScheme.onSurface + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + val scrollState = rememberScrollState() + Column(modifier = Modifier.padding(innerPadding).padding(start = 20.dp, end = 16.dp).verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.rounded_responsive_layout_24), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.use_wide_layout), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(vm.feed?.useWideLayout == true) } + Switch(checked = checked, modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.useWideLayout = checked } } - // auto skip - Column { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AutoSkipDialog(onDismiss = { showDialog.value = false }) - Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.pref_feed_skip), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) + ) + } + Text(text = stringResource(R.string.use_wide_layout_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + if ((vm.feed?.id ?: 0) > MAX_SYNTHETIC_ID) { + // refresh + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.keep_updated), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(vm.feed?.keepUpdated == true) } + Switch(checked = checked, modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.keepUpdated = checked } } - Text(text = stringResource(R.string.pref_feed_skip_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + ) + } + Text(text = stringResource(R.string.keep_updated_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + Column { + var showDialog by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(vm.feed?.audioTypeSetting?.tag ?: Feed.AudioType.SPEECH.tag) } + if (showDialog) SetAudioType(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_audio_type), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { + selectedOption = vm.feed!!.audioTypeSetting.tag + showDialog = true + }) + ) + } + Text(text = stringResource(R.string.pref_feed_audio_type_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + if ((vm.feed?.id ?: 0) >= MAX_NATURAL_SYNTHETIC_ID && vm.feed?.hasVideoMedia == true) { + // video mode + Column { + Row(Modifier.fillMaxWidth()) { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) VideoModeDialog(initMode = vm.feed?.videoModePolicy, onDismissRequest = { showDialog = false }) { mode -> + vm.feed = upsertBlk(vm.feed!!) { it.videoModePolicy = mode } + getVideoModePolicy() } - // volume adaption - Column { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) VolumeAdaptionDialog(onDismissRequest = { showDialog.value = false }) - Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.feed_volume_adapdation), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) + Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.feed_video_mode_label), style = CustomTextStyles.titleCustom, color = textColor, modifier = Modifier.clickable(onClick = { showDialog = true })) + } + Text(text = stringResource(vm.videoModeSummaryResId), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + if (vm.feed?.type != Feed.FeedType.YOUTUBE.name) { + // prefer streaming + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_stream), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_stream_over_download_title), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(vm.feed?.prefStreamOverDownload == true) } + Switch(checked = checked, modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.prefStreamOverDownload = checked } } - Text(text = stringResource(R.string.feed_volume_adaptation_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + ) + } + Text(text = stringResource(R.string.pref_stream_over_download_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + if (vm.feed?.type == Feed.FeedType.YOUTUBE.name) { + // audio quality + Column { + var showDialog by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(vm.feed?.audioQualitySetting?.tag ?: Feed.AVQuality.GLOBAL.tag) } + if (showDialog) SetAudioQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_audio_quality), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { + selectedOption = vm.feed!!.audioQualitySetting.tag + showDialog = true + }) + ) + } + Text(text = stringResource(R.string.pref_feed_audio_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + if (vm.feed?.videoModePolicy != VideoMode.AUDIO_ONLY) { + // video quality + Column { + var showDialog by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(vm.feed?.videoQualitySetting?.tag ?: Feed.AVQuality.GLOBAL.tag) } + if (showDialog) SetVideoQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_video_quality), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { + selectedOption = vm.feed!!.videoQualitySetting.tag + showDialog = true + }) + ) } - // authentication - if ((feed?.id ?: 0) > 0 && feed?.isLocalFeed != true) { - Column { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AuthenticationDialog(onDismiss = { showDialog.value = false }) - Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor) - Spacer(modifier = Modifier.width(20.dp)) - Text(text = stringResource(R.string.authentication_label), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) - } - Text(text = stringResource(R.string.authentication_descr), style = MaterialTheme.typography.bodyMedium, color = textColor) + Text(text = stringResource(R.string.pref_feed_video_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + } + // associated queue + Column { + vm.curPrefQueue = vm.feed?.queueTextExt ?: "Default" + var showDialog by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(vm.feed?.queueText ?: "Default") } + if (showDialog) SetAssociatedQueue(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_associated_queue), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { + selectedOption = vm.feed?.queueText ?: "Default" + showDialog = true + }) + ) + } + Text(text = vm.curPrefQueue + " : " + stringResource(R.string.pref_feed_associated_queue_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // auto add new to queue + if (vm.curPrefQueue != "None") { + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = androidx.media3.session.R.drawable.media3_icon_queue_add), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.audo_add_new_queue), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(vm.feed?.autoAddNewToQueue != false) } + Switch(checked = checked, modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.autoAddNewToQueue = checked } } - } - var autoDownloadChecked by remember { mutableStateOf(feed?.autoDownload == true) } - if (isEnableAutodownload && feed?.type != Feed.FeedType.YOUTUBE.name) { - // auto download - Column { - Row(Modifier.fillMaxWidth()) { - Text(text = stringResource(R.string.auto_download_label), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - Switch(checked = autoDownloadChecked, modifier = Modifier.height(24.dp), - onCheckedChange = { - autoDownloadChecked = it - feed = upsertBlk(feed!!) { f -> f.autoDownload = autoDownloadChecked } - }) - } - if (!isEnableAutodownload) - Text(text = stringResource(R.string.auto_download_disabled_globally), style = MaterialTheme.typography.bodyMedium, color = textColor) + ) + } + Text(text = stringResource(R.string.audo_add_new_queue_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + if (vm.feed?.type != Feed.FeedType.YOUTUBE.name) { + // auto delete + Column { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AutoDeleteDialog(onDismissRequest = { showDialog.value = false }) + Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.auto_delete_label), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(vm.autoDeleteSummaryResId), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + // tags + Column { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) TagSettingDialog(feeds_ = listOf(vm.feed!!), onDismiss = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.feed_tags_label), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog = true })) + } + Text(text = stringResource(R.string.feed_tags_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // playback speed + Column { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) PlaybackSpeedDialog(listOf(vm.feed!!), initSpeed = vm.feed!!.playSpeed, maxSpeed = 3f, + onDismiss = { showDialog.value = false }) { newSpeed -> + vm.feed = upsertBlk(vm.feed!!) { it.playSpeed = newSpeed } + } + Icon(ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.playback_speed), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(R.string.pref_feed_playback_speed_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // auto skip + Column { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AutoSkipDialog(onDismiss = { showDialog.value = false }) + Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_skip), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(R.string.pref_feed_skip_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // volume adaption + Column { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) VolumeAdaptionDialog(onDismissRequest = { showDialog.value = false }) + Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.feed_volume_adapdation), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(R.string.feed_volume_adaptation_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // authentication + if ((vm.feed?.id ?: 0) > 0 && vm.feed?.isLocalFeed != true) { + Column { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AuthenticationDialog(onDismiss = { showDialog.value = false }) + Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.authentication_label), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(R.string.authentication_descr), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + var autoDownloadChecked by remember { mutableStateOf(vm.feed?.autoDownload == true) } + if (isEnableAutodownload && vm.feed?.type != Feed.FeedType.YOUTUBE.name) { + // auto download + Column { + Row(Modifier.fillMaxWidth()) { + Text(text = stringResource(R.string.auto_download_label), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + Switch(checked = autoDownloadChecked, modifier = Modifier.height(24.dp), + onCheckedChange = { + autoDownloadChecked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.autoDownload = autoDownloadChecked } + }) + } + if (!isEnableAutodownload) + Text(text = stringResource(R.string.auto_download_disabled_globally), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + } + if (autoDownloadChecked) { + // auto download policy + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AutoDownloadPolicyDialog(onDismissRequest = { showDialog.value = false }) + Text(text = stringResource(R.string.feed_auto_download_policy), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + } + // episode cache + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) SetEpisodesCacheDialog(onDismiss = { showDialog.value = false }) + Text(text = stringResource(R.string.pref_episode_cache_title), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true })) + } + Text(text = stringResource(R.string.pref_episode_cache_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // counting played + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + Text(text = stringResource(R.string.pref_auto_download_counting_played_title), style = CustomTextStyles.titleCustom, color = textColor) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(vm.feed?.countingPlayed != false) } + Switch(checked = checked, modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + vm.feed = upsertBlk(vm.feed!!) { f -> f.countingPlayed = checked } } + ) + } + Text(text = stringResource(R.string.pref_auto_download_counting_played_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // inclusive filter + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AutoDownloadFilterDialog(vm.feed?.autoDownloadFilter!!, ADLIncExc.INCLUDE, onDismiss = { showDialog.value = false }) { filter -> + vm.feed = upsertBlk(vm.feed!!) { it.autoDownloadFilter = filter } } - if (autoDownloadChecked) { - // auto download policy - Column(modifier = Modifier.padding(start = 20.dp)) { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AutoDownloadPolicyDialog(onDismissRequest = { showDialog.value = false }) - Text(text = stringResource(R.string.feed_auto_download_policy), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) - } - } - // episode cache - Column(modifier = Modifier.padding(start = 20.dp)) { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) SetEpisodesCacheDialog(onDismiss = { showDialog.value = false }) - Text(text = stringResource(R.string.pref_episode_cache_title), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true })) - } - Text(text = stringResource(R.string.pref_episode_cache_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - // counting played - Column(modifier = Modifier.padding(start = 20.dp)) { - Row(Modifier.fillMaxWidth()) { - Text(text = stringResource(R.string.pref_auto_download_counting_played_title), style = CustomTextStyles.titleCustom, color = textColor) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.countingPlayed != false) } - Switch(checked = checked, modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> f.countingPlayed = checked } - } - ) - } - Text(text = stringResource(R.string.pref_auto_download_counting_played_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - // inclusive filter - Column(modifier = Modifier.padding(start = 20.dp)) { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AutoDownloadFilterDialog(feed?.autoDownloadFilter!!, ADLIncExc.INCLUDE, onDismiss = { showDialog.value = false }) { filter -> - feed = upsertBlk(feed!!) { it.autoDownloadFilter = filter } - } - Text(text = stringResource(R.string.episode_inclusive_filters_label), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true }) - ) - } - Text(text = stringResource(R.string.episode_filters_description), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - // exclusive filter - Column(modifier = Modifier.padding(start = 20.dp)) { - Row(Modifier.fillMaxWidth()) { - val showDialog = remember { mutableStateOf(false) } - if (showDialog.value) AutoDownloadFilterDialog(feed?.autoDownloadFilter!!, ADLIncExc.EXCLUDE, onDismiss = { showDialog.value = false }) { filter -> - feed = upsertBlk(feed!!) { it.autoDownloadFilter = filter } - } - Text(text = stringResource(R.string.episode_exclusive_filters_label), style = CustomTextStyles.titleCustom, color = textColor, - modifier = Modifier.clickable(onClick = { showDialog.value = true }) - ) - } - Text(text = stringResource(R.string.episode_filters_description), style = MaterialTheme.typography.bodyMedium, color = textColor) - } - + Text(text = stringResource(R.string.episode_inclusive_filters_label), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true }) + ) + } + Text(text = stringResource(R.string.episode_filters_description), style = MaterialTheme.typography.bodyMedium, color = textColor) + } + // exclusive filter + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + val showDialog = remember { mutableStateOf(false) } + if (showDialog.value) AutoDownloadFilterDialog(vm.feed?.autoDownloadFilter!!, ADLIncExc.EXCLUDE, onDismiss = { showDialog.value = false }) { filter -> + vm.feed = upsertBlk(vm.feed!!) { it.autoDownloadFilter = filter } } + Text(text = stringResource(R.string.episode_exclusive_filters_label), style = CustomTextStyles.titleCustom, color = textColor, + modifier = Modifier.clickable(onClick = { showDialog.value = true }) + ) } + Text(text = stringResource(R.string.episode_filters_description), style = MaterialTheme.typography.bodyMedium, color = textColor) } + } } } -// if (feed != null) toolbar.subtitle = feed!!.title - return composeView } override fun onDestroyView() { Logd(TAG, "onDestroyView") // _binding = null - feed = null - queues = null + vm.feed = null + vm.queues = null super.onDestroyView() } @@ -434,7 +436,7 @@ class FeedSettingsFragment : Fragment() { TopAppBar(title = { Column { Text(text = stringResource(R.string.feed_settings_label), fontSize = 20.sp, fontWeight = FontWeight.Bold) - if (!feed?.title.isNullOrBlank()) Text(text = feed!!.title!!, fontSize = 16.sp) + if (!vm.feed?.title.isNullOrBlank()) Text(text = vm.feed!!.title!!, fontSize = 16.sp) } }, navigationIcon = { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } @@ -442,46 +444,46 @@ class FeedSettingsFragment : Fragment() { } private fun getVideoModePolicy() { - when (feed?.videoModePolicy) { + when (vm.feed?.videoModePolicy) { VideoMode.NONE -> { - videoModeSummaryResId = R.string.global_default - videoMode = VideoMode.NONE.tag + vm.videoModeSummaryResId = R.string.global_default + vm.videoMode = VideoMode.NONE.tag } VideoMode.WINDOW_VIEW -> { - videoModeSummaryResId = R.string.feed_video_mode_window - videoMode = VideoMode.WINDOW_VIEW.tag + vm.videoModeSummaryResId = R.string.feed_video_mode_window + vm.videoMode = VideoMode.WINDOW_VIEW.tag } VideoMode.FULL_SCREEN_VIEW -> { - videoModeSummaryResId = R.string.feed_video_mode_fullscreen - videoMode = VideoMode.FULL_SCREEN_VIEW.tag + vm.videoModeSummaryResId = R.string.feed_video_mode_fullscreen + vm.videoMode = VideoMode.FULL_SCREEN_VIEW.tag } VideoMode.AUDIO_ONLY -> { - videoModeSummaryResId = R.string.feed_video_mode_audioonly - videoMode = VideoMode.AUDIO_ONLY.tag + vm.videoModeSummaryResId = R.string.feed_video_mode_audioonly + vm.videoMode = VideoMode.AUDIO_ONLY.tag } else -> {} } } private fun getAutoDeletePolicy() { - when (feed?.autoDeleteAction) { + when (vm.feed?.autoDeleteAction) { AutoDeleteAction.GLOBAL -> { - autoDeleteSummaryResId = R.string.global_default - autoDeletePolicy = AutoDeleteAction.GLOBAL.tag + vm.autoDeleteSummaryResId = R.string.global_default + vm.autoDeletePolicy = AutoDeleteAction.GLOBAL.tag } AutoDeleteAction.ALWAYS -> { - autoDeleteSummaryResId = R.string.feed_auto_download_always - autoDeletePolicy = AutoDeleteAction.ALWAYS.tag + vm.autoDeleteSummaryResId = R.string.feed_auto_download_always + vm.autoDeletePolicy = AutoDeleteAction.ALWAYS.tag } AutoDeleteAction.NEVER -> { - autoDeleteSummaryResId = R.string.feed_auto_download_never - autoDeletePolicy = AutoDeleteAction.NEVER.tag + vm.autoDeleteSummaryResId = R.string.feed_auto_download_never + vm.autoDeletePolicy = AutoDeleteAction.NEVER.tag } else -> {} } } @Composable fun AutoDeleteDialog(onDismissRequest: () -> Unit) { - val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) } + val (selectedOption, onOptionSelected) = remember { mutableStateOf(vm.autoDeletePolicy) } Dialog(onDismissRequest = { onDismissRequest() }) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -498,7 +500,7 @@ class FeedSettingsFragment : Fragment() { AutoDeleteAction.NEVER.tag -> AutoDeleteAction.NEVER else -> AutoDeleteAction.GLOBAL } - feed = upsertBlk(feed!!) { it.autoDeleteAction = action_ } + vm.feed = upsertBlk(vm.feed!!) { it.autoDeleteAction = action_ } getAutoDeletePolicy() onDismissRequest() } @@ -514,7 +516,7 @@ class FeedSettingsFragment : Fragment() { @Composable fun VolumeAdaptionDialog(onDismissRequest: () -> Unit) { - val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } + val (selectedOption, onOptionSelected) = remember { mutableStateOf(vm.feed?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } Dialog(onDismissRequest = { onDismissRequest() }) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -525,7 +527,7 @@ class FeedSettingsFragment : Fragment() { Logd(TAG, "row clicked: $item $selectedOption") if (item != selectedOption) { onOptionSelected(item) - feed = upsertBlk(feed!!) { it.volumeAdaptionSetting = item } + vm.feed = upsertBlk(vm.feed!!) { it.volumeAdaptionSetting = item } onDismissRequest() } } @@ -540,7 +542,7 @@ class FeedSettingsFragment : Fragment() { @Composable fun AutoDownloadPolicyDialog(onDismissRequest: () -> Unit) { - val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) } + val (selectedOption, onOptionSelected) = remember { mutableStateOf(vm.feed?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) } AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = { onDismissRequest() }, title = { Text(stringResource(R.string.pref_custom_media_dir_title), style = CustomTextStyles.titleCustom) }, text = { @@ -566,7 +568,7 @@ class FeedSettingsFragment : Fragment() { confirmButton = { TextButton(onClick = { Logd(TAG, "autoDLPolicy: ${selectedOption.name} ${selectedOption.replace}") - feed = upsertBlk(feed!!) { it.autoDLPolicy = selectedOption } + vm.feed = upsertBlk(vm.feed!!) { it.autoDLPolicy = selectedOption } onDismissRequest() }) { Text(stringResource(R.string.confirm_label)) } }, @@ -579,12 +581,12 @@ class FeedSettingsFragment : Fragment() { Dialog(onDismissRequest = onDismiss) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - var newCache by remember { mutableStateOf((feed?.autoDLMaxEpisodes ?: 1).toString()) } + var newCache by remember { mutableStateOf((vm.feed?.autoDLMaxEpisodes ?: 1).toString()) } TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text(stringResource(R.string.max_episodes_cache)) }) Button(onClick = { if (newCache.isNotEmpty()) { - feed = upsertBlk(feed!!) { it.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 } + vm.feed = upsertBlk(vm.feed!!) { it.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 } onDismiss() } }) { Text(stringResource(R.string.confirm_label)) } @@ -607,18 +609,18 @@ class FeedSettingsFragment : Fragment() { if (isChecked) Logd(TAG, "$option is checked") when (selected) { "Default" -> { - feed = upsertBlk(feed!!) { it.queueId = 0L } - curPrefQueue = selected + vm.feed = upsertBlk(vm.feed!!) { it.queueId = 0L } + vm.curPrefQueue = selected onDismissRequest() } "Active" -> { - feed = upsertBlk(feed!!) { it.queueId = -1L } - curPrefQueue = selected + vm.feed = upsertBlk(vm.feed!!) { it.queueId = -1L } + vm.curPrefQueue = selected onDismissRequest() } "None" -> { - feed = upsertBlk(feed!!) { it.queueId = -2L } - curPrefQueue = selected + vm.feed = upsertBlk(vm.feed!!) { it.queueId = -2L } + vm.curPrefQueue = selected onDismissRequest() } "Custom" -> {} @@ -629,13 +631,13 @@ class FeedSettingsFragment : Fragment() { } } if (selected == "Custom") { - if (queues == null) queues = realm.query(PlayQueue::class).find() - Logd(TAG, "queues: ${queues?.size}") - Spinner(items = queues!!.map { it.name }, selectedItem = feed?.queue?.name ?: "Default") { index -> - Logd(TAG, "Queue selected: $queues[index].name") - val q = queues!![index] - feed = upsertBlk(feed!!) { it.queue = q } - curPrefQueue = q.name + if (vm.queues == null) vm.queues = realm.query(PlayQueue::class).find() + Logd(TAG, "queues: ${vm.queues?.size}") + Spinner(items = vm.queues!!.map { it.name }, selectedItem = vm.feed?.queue?.name ?: "Default") { index -> + Logd(TAG, "Queue selected: ${vm.queues!![index].name}") + val q = vm.queues!![index] + vm.feed = upsertBlk(vm.feed!!) { it.queue = q } + vm.curPrefQueue = q.name onDismissRequest() } } @@ -657,7 +659,7 @@ class FeedSettingsFragment : Fragment() { selected = option.tag if (isChecked) Logd(TAG, "$option is checked") val type = Feed.AudioType.fromTag(selected) - feed = upsertBlk(feed!!) { it.audioType = type.code } + vm.feed = upsertBlk(vm.feed!!) { it.audioType = type.code } onDismissRequest() } ) @@ -682,7 +684,7 @@ class FeedSettingsFragment : Fragment() { selected = option.tag if (isChecked) Logd(TAG, "$option is checked") val type = Feed.AVQuality.fromTag(selected) - feed = upsertBlk(feed!!) { it.audioQuality = type.code } + vm.feed = upsertBlk(vm.feed!!) { it.audioQuality = type.code } onDismissRequest() }) Text(option.tag) @@ -706,7 +708,7 @@ class FeedSettingsFragment : Fragment() { selected = option.tag if (isChecked) Logd(TAG, "$option is checked") val type = Feed.AVQuality.fromTag(selected) - feed = upsertBlk(feed!!) { it.videoQuality = type.code } + vm.feed = upsertBlk(vm.feed!!) { it.videoQuality = type.code } onDismissRequest() }) Text(option.tag) @@ -722,19 +724,19 @@ class FeedSettingsFragment : Fragment() { Dialog(onDismissRequest = onDismiss) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - val oldName = feed?.username?:"" + val oldName = vm.feed?.username?:"" var newName by remember { mutableStateOf(oldName) } TextField(value = newName, onValueChange = { newName = it }, label = { Text("Username") }) - val oldPW = feed?.password?:"" + val oldPW = vm.feed?.password?:"" var newPW by remember { mutableStateOf(oldPW) } TextField(value = newPW, onValueChange = { newPW = it }, label = { Text("Password") }) Button(onClick = { if (newName.isNotEmpty() && oldName != newName) { - feed = upsertBlk(feed!!) { + vm.feed = upsertBlk(vm.feed!!) { it.username = newName it.password = newPW } - Thread({ runOnce(requireContext(), feed) }, "RefreshAfterCredentialChange").start() + Thread({ runOnce(requireContext(), vm.feed) }, "RefreshAfterCredentialChange").start() onDismiss() } }) { Text(stringResource(R.string.confirm_label)) } @@ -748,15 +750,15 @@ class FeedSettingsFragment : Fragment() { Dialog(onDismissRequest = onDismiss) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - var intro by remember { mutableStateOf((feed?.introSkip ?: 0).toString()) } + var intro by remember { mutableStateOf((vm.feed?.introSkip ?: 0).toString()) } TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip first (seconds)") }) - var ending by remember { mutableStateOf((feed?.endingSkip ?: 0).toString()) } + var ending by remember { mutableStateOf((vm.feed?.endingSkip ?: 0).toString()) } TextField(value = ending, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) ending = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip last (seconds)") }) Button(onClick = { if (intro.isNotEmpty() || ending.isNotEmpty()) { - feed = upsertBlk(feed!!) { + vm.feed = upsertBlk(vm.feed!!) { it.introSkip = intro.toIntOrNull() ?: 0 it.endingSkip = ending.toIntOrNull() ?: 0 } @@ -873,8 +875,8 @@ class FeedSettingsFragment : Fragment() { @JvmName("setFeedFunction") fun setFeed(feed_: Feed) { - feed = feed_ - feed = getFeed(feed_.id) + vm.feed = feed_ + vm.feed = getFeed(feed_.id) // if (feed!!.preferences == null) { // feed!!.preferences = FeedPreferences(feed!!.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") // persistFeedPreferences(feed!!) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt index 43957023..399418e0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt @@ -65,71 +65,73 @@ import kotlinx.coroutines.withContext import java.util.* class LogsFragment : Fragment() { - private val shareLogs = mutableStateListOf() - private val subscriptionLogs = mutableStateListOf() - private val downloadLogs = mutableStateListOf() - private var title by mutableStateOf("") - private var displayUpArrow = false + class LogsVM { + internal val shareLogs = mutableStateListOf() + internal val subscriptionLogs = mutableStateListOf() + internal val downloadLogs = mutableStateListOf() + internal var title by mutableStateOf("") + internal var displayUpArrow = false + internal var showDeleteConfirmDialog = mutableStateOf(false) + } - private var showDeleteConfirmDialog = mutableStateOf(false) + private val vm = LogsVM() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + vm.displayUpArrow = parentFragmentManager.backStackEntryCount != 0 + if (savedInstanceState != null) vm.displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - ComfirmDialog(R.string.confirm_delete_logs_label, stringResource(R.string.confirm_delete_logs_message), showDeleteConfirmDialog) { - runOnIOScope { - when { - shareLogs.isNotEmpty() -> { - realm.write { - val items = query(ShareLog::class).find() - delete(items) - } - shareLogs.clear() - loadShareLog() - } - subscriptionLogs.isNotEmpty() -> { - realm.write { - val items = query(SubscriptionLog::class).find() - delete(items) - } - subscriptionLogs.clear() - loadSubscriptionLog() - } - downloadLogs.isNotEmpty() -> { - realm.write { - val items = query(DownloadResult::class).find() - delete(items) - } - downloadLogs.clear() - loadDownloadLog() - } - } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { LogsScreen() } } } + loadDownloadLog() + return composeView + } + + @Composable + fun LogsScreen() { + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + ComfirmDialog(R.string.confirm_delete_logs_label, stringResource(R.string.confirm_delete_logs_message), vm.showDeleteConfirmDialog) { + runOnIOScope { + when { + vm.shareLogs.isNotEmpty() -> { + realm.write { + val items = query(ShareLog::class).find() + delete(items) + } + vm.shareLogs.clear() + loadShareLog() + } + vm.subscriptionLogs.isNotEmpty() -> { + realm.write { + val items = query(SubscriptionLog::class).find() + delete(items) } + vm.subscriptionLogs.clear() + loadSubscriptionLog() } - when { - downloadLogs.isNotEmpty() -> DownloadLogView() - shareLogs.isNotEmpty() -> SharedLogView() - subscriptionLogs.isNotEmpty() -> SubscriptionLogView() + vm.downloadLogs.isNotEmpty() -> { + realm.write { + val items = query(DownloadResult::class).find() + delete(items) + } + vm.downloadLogs.clear() + loadDownloadLog() } } } } + when { + vm.downloadLogs.isNotEmpty() -> DownloadLogView() + vm.shareLogs.isNotEmpty() -> SharedLogView() + vm.subscriptionLogs.isNotEmpty() -> SubscriptionLogView() + } } } - loadDownloadLog() - return composeView } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) + outState.putBoolean(KEY_UP_ARROW, vm.displayUpArrow) super.onSaveInstanceState(outState) } @@ -153,7 +155,7 @@ class LogsFragment : Fragment() { LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 6.dp, top = 5.dp, bottom = 5.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(shareLogs) { position, log -> + itemsIndexed(vm.shareLogs) { position, log -> val textColor = MaterialTheme.colorScheme.onSurface Row (modifier = Modifier.clickable { if (log.status < ShareLog.Status.SUCCESS.ordinal) { @@ -233,7 +235,7 @@ class LogsFragment : Fragment() { LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 6.dp, top = 5.dp, bottom = 5.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(subscriptionLogs) { position, log -> + itemsIndexed(vm.subscriptionLogs) { position, log -> val textColor = MaterialTheme.colorScheme.onSurface Row (verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp).clickable { dialogParam.value = log @@ -260,7 +262,7 @@ class LogsFragment : Fragment() { LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 6.dp, top = 5.dp, bottom = 5.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(downloadLogs) { position, status -> + itemsIndexed(vm.downloadLogs) { position, status -> val textColor = MaterialTheme.colorScheme.onSurface Row (modifier = Modifier.clickable { showDialog.value = true @@ -288,7 +290,7 @@ class LogsFragment : Fragment() { } fun newerWasSuccessful(downloadStatusIndex: Int, feedTypeId: Int, id: Long): Boolean { for (i in 0 until downloadStatusIndex) { - val status_: DownloadResult = downloadLogs[i] + val status_: DownloadResult = vm.downloadLogs[i] if (status_.feedfileType == feedTypeId && status_.feedfileId == id && status_.isSuccessful) return true } return false @@ -324,34 +326,34 @@ class LogsFragment : Fragment() { } private fun clearAllLogs() { - subscriptionLogs.clear() - shareLogs.clear() - downloadLogs.clear() + vm.subscriptionLogs.clear() + vm.shareLogs.clear() + vm.downloadLogs.clear() } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyTopAppBar() { - TopAppBar(title = { Text(title) }, - navigationIcon = if (displayUpArrow) { + TopAppBar(title = { Text(vm.title) }, + navigationIcon = if (vm.displayUpArrow) { { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } else { { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_history), contentDescription = "Open Drawer") } } }, actions = { - if (title != "Downloads log") IconButton(onClick = { + if (vm.title != "Downloads log") IconButton(onClick = { clearAllLogs() loadDownloadLog() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "download") } - if (title != "Shares log") IconButton(onClick = { + if (vm.title != "Shares log") IconButton(onClick = { clearAllLogs() loadShareLog() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_share), contentDescription = "share") } - if (title != "Subscriptions log") IconButton(onClick = { + if (vm.title != "Subscriptions log") IconButton(onClick = { clearAllLogs() loadSubscriptionLog() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_subscriptions), contentDescription = "subscriptions") } - IconButton(onClick = { showDeleteConfirmDialog.value = true + IconButton(onClick = { vm.showDeleteConfirmDialog.value = true }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_delete), contentDescription = "clear history") } } ) @@ -365,8 +367,8 @@ class LogsFragment : Fragment() { realm.query(ShareLog::class).sort("id", Sort.DESCENDING).find().toMutableList() } withContext(Dispatchers.Main) { - shareLogs.addAll(result) - title = "Shares log" + vm.shareLogs.addAll(result) + vm.title = "Shares log" } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } @@ -380,8 +382,8 @@ class LogsFragment : Fragment() { realm.query(SubscriptionLog::class).sort("id", Sort.DESCENDING).find().toMutableList() } withContext(Dispatchers.Main) { - subscriptionLogs.addAll(result) - title = "Subscriptions log" + vm.subscriptionLogs.addAll(result) + vm.title = "Subscriptions log" } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } @@ -398,8 +400,8 @@ class LogsFragment : Fragment() { dlog } withContext(Dispatchers.Main) { - downloadLogs.addAll(result) - title = "Downloads log" + vm.downloadLogs.addAll(result) + vm.title = "Downloads log" } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index 4857c231..991b88b2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -62,12 +62,13 @@ import kotlin.math.roundToInt class NavDrawerFragment : Fragment() { val TAG = this::class.simpleName ?: "Anonymous" + private val feeds = mutableStateListOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) getRecentPodcasts() - val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { NavDrawerScreen() } } } Logd(TAG, "fragment onCreateView") setupDrawerRoundBackground(composeView) @@ -95,7 +96,7 @@ class NavDrawerFragment : Fragment() { } @Composable - fun MainView() { + fun NavDrawerScreen() { Column(modifier = Modifier.padding(start = 20.dp, end = 10.dp, top = 20.dp, bottom = 20.dp), verticalArrangement = Arrangement.spacedBy(15.dp)) { val textColor = MaterialTheme.colorScheme.onSurface for (nav in navMap.values) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineEpisodesFragment.kt index 7daf917c..171b0d38 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineEpisodesFragment.kt @@ -28,70 +28,74 @@ import androidx.fragment.app.Fragment import kotlin.math.min class OnlineEpisodesFragment: Fragment() { - private var displayUpArrow = false - private var infoBarText = mutableStateOf("") - private lateinit var swipeActions: SwipeActions - private var leftActionState = mutableStateOf(NoActionSwipeAction()) - private var rightActionState = mutableStateOf(NoActionSwipeAction()) + class OnlineEpisodesVM { + internal var displayUpArrow = false - private var showSwipeActionsDialog by mutableStateOf(false) + internal var infoBarText = mutableStateOf("") + internal lateinit var swipeActions: SwipeActions + internal var leftActionState = mutableStateOf(NoActionSwipeAction()) + internal var rightActionState = mutableStateOf(NoActionSwipeAction()) - val episodes = mutableListOf() - val vms = mutableStateListOf() + internal var showSwipeActionsDialog by mutableStateOf(false) + + internal val episodes = mutableListOf() + internal val vms = mutableStateListOf() + } + + private val vm = OnlineEpisodesVM() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) Logd(TAG, "fragment onCreateView") - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) - - swipeActions = SwipeActions(this, TAG) - leftActionState.value = swipeActions.actions.left[0] - rightActionState.value = swipeActions.actions.right[0] - lifecycle.addObserver(swipeActions) - - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (showSwipeActionsDialog) SwipeActionsSettingDialog(swipeActions, onDismissRequest = { showSwipeActionsDialog = false }) { actions -> - swipeActions.actions = actions - refreshSwipeTelltale() - } - swipeActions.ActionOptionsDialog() - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { showSwipeActionsDialog = true }) - EpisodeLazyColumn(activity as MainActivity, vms = vms, - buildMoreItems = { buildMoreItems() }, - leftSwipeCB = { - if (leftActionState.value is NoActionSwipeAction) showSwipeActionsDialog = true - else leftActionState.value.performAction(it) - }, - rightSwipeCB = { - if (rightActionState.value is NoActionSwipeAction) showSwipeActionsDialog = true - else rightActionState.value.performAction(it) - }, - ) - } - } - } - } - } + vm.displayUpArrow = parentFragmentManager.backStackEntryCount != 0 + if (savedInstanceState != null) vm.displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + + vm.swipeActions = SwipeActions(this, TAG) + vm.leftActionState.value = vm.swipeActions.actions.left[0] + vm.rightActionState.value = vm.swipeActions.actions.right[0] + lifecycle.addObserver(vm.swipeActions) + + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { OnlineEpisodesScreen() } } } refreshSwipeTelltale() return composeView } + @Composable + fun OnlineEpisodesScreen() { + if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> + vm.swipeActions.actions = actions + refreshSwipeTelltale() + } + vm.swipeActions.ActionOptionsDialog() + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, + buildMoreItems = { buildMoreItems() }, + leftSwipeCB = { + if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionState.value.performAction(it) + }, + rightSwipeCB = { + if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionState.value.performAction(it) + }, + ) + } + } + } + fun buildMoreItems() { - val nextItems = (vms.size until min(vms.size + VMS_CHUNK_SIZE, episodes.size)).map { EpisodeVM(episodes[it], FeedEpisodesFragment.Companion.TAG) } - if (nextItems.isNotEmpty()) vms.addAll(nextItems) + val nextItems = (vm.vms.size until min(vm.vms.size + VMS_CHUNK_SIZE, vm.episodes.size)).map { EpisodeVM(vm.episodes[it], FeedEpisodesFragment.Companion.TAG) } + if (nextItems.isNotEmpty()) vm.vms.addAll(nextItems) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyTopAppBar() { TopAppBar(title = { Text(stringResource(R.string.online_episodes_label)) }, - navigationIcon = if (displayUpArrow) { + navigationIcon = if (vm.displayUpArrow) { { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } else { { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(Icons.Filled.Menu, contentDescription = "Open Drawer") } } @@ -101,33 +105,33 @@ class OnlineEpisodesFragment: Fragment() { override fun onStart() { super.onStart() - stopMonitor(vms) - vms.clear() + stopMonitor(vm.vms) + vm.vms.clear() buildMoreItems() // for (e in episodes) { vms.add(EpisodeVM(e, TAG)) } - infoBarText.value = "${episodes.size} episodes" + vm.infoBarText.value = "${vm.episodes.size} episodes" } override fun onDestroyView() { Logd(TAG, "onDestroyView") - episodes.clear() - stopMonitor(vms) - vms.clear() + vm.episodes.clear() + stopMonitor(vm.vms) + vm.vms.clear() super.onDestroyView() } private fun refreshSwipeTelltale() { - leftActionState.value = swipeActions.actions.left[0] - rightActionState.value = swipeActions.actions.right[0] + vm.leftActionState.value = vm.swipeActions.actions.left[0] + vm.rightActionState.value = vm.swipeActions.actions.right[0] } fun setEpisodes(episodeList_: MutableList) { - episodes.clear() - episodes.addAll(episodeList_) + vm.episodes.clear() + vm.episodes.addAll(episodeList_) } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) + outState.putBoolean(KEY_UP_ARROW, vm.displayUpArrow) super.onSaveInstanceState(outState) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index a86db83e..09722a4e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -81,78 +81,74 @@ import java.util.* */ class OnlineFeedFragment : Fragment() { val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE) } + var feedSource: String = "" - private var displayUpArrow = false + class OnlineFeedVM { + internal var displayUpArrow = false - var feedSource: String = "" - private var feedUrl: String = "" - private var urlToLog: String = "" - private lateinit var feedBuilder: FeedBuilder - private var showYTChannelDialog by mutableStateOf(false) - - private var isShared: Boolean = false - - private var showFeedDisplay by mutableStateOf(false) - private var showProgress by mutableStateOf(true) - private var autoDownloadChecked by mutableStateOf(false) - private var enableSubscribe by mutableStateOf(true) - private var enableEpisodes by mutableStateOf(true) - private var subButTextRes by mutableIntStateOf(R.string.subscribe_label) - - private val feedId: Long - get() { - if (feeds == null) return 0 - for (f in feeds!!) { - if (f.downloadUrl == selectedDownloadUrl) return f.id + internal var feedUrl: String = "" + internal var urlToLog: String = "" + internal lateinit var feedBuilder: FeedBuilder + internal var showYTChannelDialog by mutableStateOf(false) + + internal var isShared: Boolean = false + + internal var showFeedDisplay by mutableStateOf(false) + internal var showProgress by mutableStateOf(true) + internal var autoDownloadChecked by mutableStateOf(false) + internal var enableSubscribe by mutableStateOf(true) + internal var enableEpisodes by mutableStateOf(true) + internal var subButTextRes by mutableIntStateOf(R.string.subscribe_label) + + internal val feedId: Long + get() { + if (feeds == null) return 0 + for (f in feeds!!) if (f.downloadUrl == selectedDownloadUrl) return f.id + return 0 } - return 0 - } - @Volatile - private var feeds: List? = null - private var feed by mutableStateOf(null) - private var selectedDownloadUrl: String? = null -// private var downloader: Downloader? = null - private var username: String? = null - private var password: String? = null + @Volatile + internal var feeds: List? = null + internal var feed by mutableStateOf(null) + internal var selectedDownloadUrl: String? = null + // private var downloader: Downloader? = null + internal var username: String? = null + internal var password: String? = null + + internal var isPaused = false + internal var didPressSubscribe = false + internal var isFeedFoundBySearch = false - private var isPaused = false - private var didPressSubscribe = false - private var isFeedFoundBySearch = false + internal var dialog: Dialog? = null + } - private var dialog: Dialog? = null + private val vm = OnlineFeedVM() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) - - feedUrl = requireArguments().getString(ARG_FEEDURL) ?: "" - isShared = requireArguments().getBoolean("isShared") - Logd(TAG, "feedUrl: $feedUrl") - feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) } - - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (showYTChannelDialog) feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = { showYTChannelDialog = false }) { feed, map -> handleFeed(feed, map) } - MainView() - } - } - } - if (feedUrl.isEmpty()) { + vm.displayUpArrow = parentFragmentManager.backStackEntryCount != 0 + if (savedInstanceState != null) vm.displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + + vm.feedUrl = requireArguments().getString(ARG_FEEDURL) ?: "" + vm.isShared = requireArguments().getBoolean("isShared") + Logd(TAG, "feedUrl: ${vm.feedUrl}") + vm.feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) } + + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { OnlineFeedScreen() } } } + + if (vm.feedUrl.isEmpty()) { Log.e(TAG, "feedUrl is null.") showNoPodcastFoundError() } else { - Logd(TAG, "Activity was started with url $feedUrl") - showProgress = true + Logd(TAG, "Activity was started with url ${vm.feedUrl}") + vm.showProgress = true // Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL - if (feedUrl.contains("subscribeonandroid.com")) feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "") + if (vm.feedUrl.contains("subscribeonandroid.com")) vm.feedUrl = vm.feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "") if (savedInstanceState != null) { - username = savedInstanceState.getString("username") - password = savedInstanceState.getString("password") + vm.username = savedInstanceState.getString("username") + vm.password = savedInstanceState.getString("password") } - lookupUrlAndBuild(feedUrl) + lookupUrlAndBuild(vm.feedUrl) } return composeView } @@ -161,7 +157,7 @@ class OnlineFeedFragment : Fragment() { @Composable fun MyTopAppBar() { TopAppBar(title = { Text(text = "") }, - navigationIcon = if (displayUpArrow) { + navigationIcon = if (vm.displayUpArrow) { { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } else { { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(Icons.Filled.Menu, contentDescription = "Open Drawer") } } @@ -171,35 +167,35 @@ class OnlineFeedFragment : Fragment() { override fun onStart() { super.onStart() - isPaused = false + vm.isPaused = false procFlowEvents() } override fun onStop() { super.onStop() - isPaused = true + vm.isPaused = true cancelFlowEvents() // if (downloader != null && !downloader!!.isFinished) downloader!!.cancel() - if (dialog != null && dialog!!.isShowing) dialog!!.dismiss() + if (vm.dialog != null && vm.dialog!!.isShowing) vm.dialog!!.dismiss() } override fun onDestroy() { - feeds = null + vm.feeds = null super.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) + outState.putBoolean(KEY_UP_ARROW, vm.displayUpArrow) super.onSaveInstanceState(outState) - outState.putString("username", username) - outState.putString("password", password) + outState.putString("username", vm.username) + outState.putString("password", vm.password) } private fun handleFeed(feed_: Feed, map: Map) { - selectedDownloadUrl = feedBuilder.selectedDownloadUrl - feed = feed_ - if (isShared) { - val log = realm.query(ShareLog::class).query("url == $0", urlToLog).first().find() + vm.selectedDownloadUrl = vm.feedBuilder.selectedDownloadUrl + vm.feed = feed_ + if (vm.isShared) { + val log = realm.query(ShareLog::class).query("url == $0", vm.urlToLog).first().find() if (log != null) upsertBlk(log) { it.title = feed_.title it.author = feed_.author @@ -210,21 +206,21 @@ class OnlineFeedFragment : Fragment() { private fun lookupUrlAndBuild(url: String) { CoroutineScope(Dispatchers.IO).launch { - urlToLog = url + vm.urlToLog = url val urlString = PodcastSearcherRegistry.lookupUrl1(url) Logd(TAG, "lookupUrlAndBuild: urlString: ${urlString}") try { - feeds = getFeedList() - if (feedBuilder.isYoutube(urlString)) { - if (feedBuilder.isYoutubeChannel()) { - val nTabs = feedBuilder.youtubeChannelValidTabs() - if (nTabs > 1) showYTChannelDialog = true - else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) } - } else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) } + vm.feeds = getFeedList() + if (vm.feedBuilder.isYoutube(urlString)) { + if (vm.feedBuilder.isYoutubeChannel()) { + val nTabs = vm.feedBuilder.youtubeChannelValidTabs() + if (nTabs > 1) vm.showYTChannelDialog = true + else vm.feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) } + } else vm.feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) } } else { val urlFinal = getFinalRedirectedUrl(urlString)?:"" Logd(TAG, "lookupUrlAndBuild: urlFinal: ${urlFinal}") - feedBuilder.buildPodcast(urlFinal, username, password) { feed_, map -> handleFeed(feed_, map) } + vm.feedBuilder.buildPodcast(urlFinal, vm.username, vm.password) { feed_, map -> handleFeed(feed_, map) } } } catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e) } catch (e: Throwable) { @@ -251,17 +247,17 @@ class OnlineFeedFragment : Fragment() { } } if (url != null) { - urlToLog = url + vm.urlToLog = url Logd(TAG, "Successfully retrieve feed url") - isFeedFoundBySearch = true - feeds = getFeedList() - if (feedBuilder.isYoutube(url)) { - if (feedBuilder.isYoutubeChannel()) { - val nTabs = feedBuilder.youtubeChannelValidTabs() - if (nTabs > 1) showYTChannelDialog = true - else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) } - } else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) } - } else feedBuilder.buildPodcast(url, username, password) { feed_, map -> handleFeed(feed_, map) } + vm.isFeedFoundBySearch = true + vm.feeds = getFeedList() + if (vm.feedBuilder.isYoutube(url)) { + if (vm.feedBuilder.isYoutubeChannel()) { + val nTabs = vm.feedBuilder.youtubeChannelValidTabs() + if (nTabs > 1) vm.showYTChannelDialog = true + else vm.feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) } + } else vm.feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) } + } else vm.feedBuilder.buildPodcast(url, vm.username, vm.password) { feed_, map -> handleFeed(feed_, map) } } else { showNoPodcastFoundError() Logd(TAG, "Failed to retrieve feed url") @@ -315,7 +311,7 @@ class OnlineFeedFragment : Fragment() { try { val feeds = withContext(Dispatchers.IO) { getFeedList() } withContext(Dispatchers.Main) { - this@OnlineFeedFragment.feeds = feeds + this@OnlineFeedFragment.vm.feeds = feeds handleUpdatedFeedStatus() } } catch (e: Throwable) { @@ -330,9 +326,9 @@ class OnlineFeedFragment : Fragment() { * This method is executed on the GUI thread. */ private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map) { - showProgress = false - showFeedDisplay = true - if (isFeedFoundBySearch) { + vm.showProgress = false + vm.showFeedDisplay = true + if (vm.isFeedFoundBySearch) { val resId = R.string.no_feed_url_podcast_found_by_search Snackbar.make(requireView(), resId, Snackbar.LENGTH_LONG).show() } @@ -368,17 +364,18 @@ class OnlineFeedFragment : Fragment() { } @Composable - fun MainView() { + fun OnlineFeedScreen() { val textColor = MaterialTheme.colorScheme.onSurface + if (vm.showYTChannelDialog) vm.feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = { vm.showYTChannelDialog = false }) { feed, map -> handleFeed(feed, map) } val feedLogsMap_ = feedLogsMap!! Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - if (showProgress) Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(innerPadding).fillMaxSize()) { + if (vm.showProgress) Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(innerPadding).fillMaxSize()) { CircularProgressIndicator(progress = {0.6f}, strokeWidth = 10.dp, color = textColor, modifier = Modifier.size(50.dp).align(Alignment.Center)) } else Column(modifier = Modifier.padding(innerPadding).fillMaxSize().padding(start = 10.dp, end = 10.dp)) { - if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(100.dp).background(MaterialTheme.colorScheme.surface)) { + if (vm.showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(100.dp).background(MaterialTheme.colorScheme.surface)) { val (coverImage, taColumn, buttons) = createRefs() - AsyncImage(model = feed?.imageUrl ?: "", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher), + AsyncImage(model = vm.feed?.imageUrl ?: "", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher), modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) { bottom.linkTo(parent.bottom) start.linkTo(parent.start) @@ -387,8 +384,8 @@ class OnlineFeedFragment : Fragment() { top.linkTo(coverImage.top) start.linkTo(coverImage.end) }) { - Text(feed?.title ?: "No title", color = textColor, style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis) - Text(feed?.author ?: "", color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(vm.feed?.title ?: "No title", color = textColor, style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis) + Text(vm.feed?.author ?: "", color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) } Row(Modifier.constrainAs(buttons) { start.linkTo(coverImage.end) @@ -396,40 +393,40 @@ class OnlineFeedFragment : Fragment() { end.linkTo(parent.end) }) { Spacer(modifier = Modifier.weight(0.2f)) - if (enableSubscribe) Button(onClick = { - if (feedInFeedlist() || isSubscribed(feed!!)) { - if (isShared) { - val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() + if (vm.enableSubscribe) Button(onClick = { + if (feedInFeedlist() || isSubscribed(vm.feed!!)) { + if (vm.isShared) { + val log = realm.query(ShareLog::class).query("url == $0", vm.feedUrl).first().find() if (log != null) upsertBlk(log) { it.status = ShareLog.Status.EXISTING.ordinal } } - val feed = getFeedByTitleAndAuthor(feed?.eigenTitle ?: "", feed?.author ?: "") + val feed = getFeedByTitleAndAuthor(vm.feed?.eigenTitle ?: "", vm.feed?.author ?: "") if (feed != null) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) } else { - enableSubscribe = false - enableEpisodes = false + vm.enableSubscribe = false + vm.enableEpisodes = false CoroutineScope(Dispatchers.IO).launch { - feedBuilder.subscribe(feed!!) - if (isShared) { - val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() + vm.feedBuilder.subscribe(vm.feed!!) + if (vm.isShared) { + val log = realm.query(ShareLog::class).query("url == $0", vm.feedUrl).first().find() if (log != null) upsertBlk(log) { it.status = ShareLog.Status.SUCCESS.ordinal } } withContext(Dispatchers.Main) { - enableSubscribe = true - didPressSubscribe = true + vm.enableSubscribe = true + vm.didPressSubscribe = true handleUpdatedFeedStatus() } } } - }) { Text(stringResource(subButTextRes)) } + }) { Text(stringResource(vm.subButTextRes)) } Spacer(modifier = Modifier.weight(0.1f)) - if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) { Text(stringResource(R.string.episodes_label)) } + if (vm.enableEpisodes && vm.feed != null) Button(onClick = { showEpisodes(vm.feed!!.episodes) }) { Text(stringResource(R.string.episodes_label)) } Spacer(modifier = Modifier.weight(0.2f)) } } Column { // alternate_urls_spinner if (feedSource != "VistaGuide" && isEnableAutodownload) Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) { - Checkbox(checked = autoDownloadChecked, onCheckedChange = { autoDownloadChecked = it }) + Checkbox(checked = vm.autoDownloadChecked, onCheckedChange = { vm.autoDownloadChecked = it }) Text(text = stringResource(R.string.auto_download_label), style = MaterialTheme.typography.bodyMedium.merge(), color = textColor, modifier = Modifier.padding(start = 16.dp) @@ -437,11 +434,11 @@ class OnlineFeedFragment : Fragment() { } } val scrollState = rememberScrollState() - var numEpisodes by remember { mutableIntStateOf(feed?.episodes?.size ?: 0) } + var numEpisodes by remember { mutableIntStateOf(vm.feed?.episodes?.size ?: 0) } LaunchedEffect(Unit) { while (true) { delay(1000) - numEpisodes = feed?.episodes?.size ?: 0 + numEpisodes = vm.feed?.episodes?.size ?: 0 } } Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).verticalScroll(scrollState)) { @@ -449,8 +446,8 @@ class OnlineFeedFragment : Fragment() { modifier = Modifier.padding(top = 5.dp, bottom = 10.dp)) Text(stringResource(R.string.description_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) - Text(HtmlToPlainText.getPlainText(feed?.description ?: ""), color = textColor, style = MaterialTheme.typography.bodyMedium) - val sLog = remember { feedLogsMap_[feed?.downloadUrl ?: ""] } + Text(HtmlToPlainText.getPlainText(vm.feed?.description ?: ""), color = textColor, style = MaterialTheme.typography.bodyMedium) + val sLog = remember { feedLogsMap_[vm.feed?.downloadUrl ?: ""] } if (sLog != null) { val commentTextState by remember { mutableStateOf(TextFieldValue(sLog.comment)) } val context = LocalContext.current @@ -467,10 +464,10 @@ class OnlineFeedFragment : Fragment() { modifier = Modifier.padding(start = 15.dp, bottom = 10.dp)) } } - Text(feed?.mostRecentItem?.title ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) - Text("${feed?.language ?: ""} ${feed?.type ?: ""} ${feed?.lastUpdate ?: ""}", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) - Text(feed?.link ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) - Text(feed?.downloadUrl ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) + Text(vm.feed?.mostRecentItem?.title ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) + Text("${vm.feed?.language ?: ""} ${vm.feed?.type ?: ""} ${vm.feed?.lastUpdate ?: ""}", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) + Text(vm.feed?.link ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) + Text(vm.feed?.downloadUrl ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp)) } } } @@ -491,56 +488,56 @@ class OnlineFeedFragment : Fragment() { private fun handleUpdatedFeedStatus() { val dli = DownloadServiceInterface.get() - if (dli == null || selectedDownloadUrl == null) return + if (dli == null || vm.selectedDownloadUrl == null) return when { // feedSource != "VistaGuide" -> { // binding.subscribeButton.isEnabled = false // } - dli.isDownloadingEpisode(selectedDownloadUrl!!) -> { - enableSubscribe = false - subButTextRes = R.string.subscribe_label + dli.isDownloadingEpisode(vm.selectedDownloadUrl!!) -> { + vm.enableSubscribe = false + vm.subButTextRes = R.string.subscribe_label } feedInFeedlist() -> { - enableSubscribe = true - subButTextRes = R.string.open - if (didPressSubscribe) { - didPressSubscribe = false - val feed1 = getFeed(feedId, true)?: return + vm.enableSubscribe = true + vm.subButTextRes = R.string.open + if (vm.didPressSubscribe) { + vm.didPressSubscribe = false + val feed1 = getFeed(vm.feedId, true)?: return // if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false, // FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") if (feedSource == "VistaGuide") { feed1.prefStreamOverDownload = true feed1.autoDownload = false } else if (isEnableAutodownload) { - val autoDownload = autoDownloadChecked + val autoDownload = vm.autoDownloadChecked feed1.autoDownload = autoDownload val editor = prefs.edit() editor?.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload) editor?.apply() } - if (username != null) { - feed1.username = username - feed1.password = password + if (vm.username != null) { + feed1.username = vm.username + feed1.password = vm.password } runOnIOScope { upsert(feed1) {} } // openFeed() } } else -> { - enableSubscribe = true - subButTextRes = R.string.subscribe_label + vm.enableSubscribe = true + vm.subButTextRes = R.string.subscribe_label } } } private fun feedInFeedlist(): Boolean { - return feedId != 0L + return vm.feedId != 0L } @UiThread private fun showErrorDialog(errorMsg: String?, details: String) { - if (!isRemoving && !isPaused) { + if (!isRemoving && !vm.isPaused) { val builder = MaterialAlertDialogBuilder(requireContext()) builder.setTitle(R.string.error_label) if (errorMsg != null) { @@ -562,8 +559,8 @@ class OnlineFeedFragment : Fragment() { // setResult(RESULT_ERROR) // finish() } - if (dialog != null && dialog!!.isShowing) dialog!!.dismiss() - dialog = builder.show() + if (vm.dialog != null && vm.dialog!!.isShowing) vm.dialog!!.dismiss() + vm.dialog = builder.show() } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt index 485c0b51..745399ae 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt @@ -61,26 +61,37 @@ import java.util.* class OnlineSearchFragment : Fragment() { val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) } - private var mainAct: MainActivity? = null - private var displayUpArrow = false + class OnlineSearchVM { + internal var mainAct: MainActivity? = null + internal var displayUpArrow = false - private var showError by mutableStateOf(false) - private var errorText by mutableStateOf("") - private var showPowerBy by mutableStateOf(false) - private var showRetry by mutableStateOf(false) - private var retryTextRes by mutableIntStateOf(0) - private var showGrid by mutableStateOf(false) + internal var showError by mutableStateOf(false) + internal var errorText by mutableStateOf("") + internal var showPowerBy by mutableStateOf(false) + internal var showRetry by mutableStateOf(false) + internal var retryTextRes by mutableIntStateOf(0) + internal var showGrid by mutableStateOf(false) - private val showOPMLRestoreDialog = mutableStateOf(false) - private var numColumns by mutableIntStateOf(4) - private val searchResult = mutableStateListOf() + internal val showOPMLRestoreDialog = mutableStateOf(false) + internal var numColumns by mutableIntStateOf(4) + internal val searchResult = mutableStateListOf() + + internal var showOpmlImportSelectionDialog by mutableStateOf(false) + internal val readElements = mutableStateListOf() + + internal var eventSink: Job? = null + internal fun cancelFlowEvents() { + eventSink?.cancel() + eventSink = null + } + } + + private val vm = OnlineSearchVM() - private var showOpmlImportSelectionDialog by mutableStateOf(false) - private val readElements = mutableStateListOf() private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> if (uri == null) return@registerForActivityResult - OpmlTransporter.startImport(requireContext(), uri) { readElements.addAll(it) } - showOpmlImportSelectionDialog = true + OpmlTransporter.startImport(requireContext(), uri) { vm.readElements.addAll(it) } + vm.showOpmlImportSelectionDialog = true } private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { uri: Uri? -> @@ -106,31 +117,31 @@ class OnlineSearchFragment : Fragment() { withContext(Dispatchers.Main) { if (feed != null) { val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id) - mainAct?.loadChildFragment(fragment) + vm.mainAct?.loadChildFragment(fragment) } } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) - mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG) + vm.mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG) } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) - mainAct = activity as? MainActivity + vm.mainAct = activity as? MainActivity Logd(TAG, "fragment onCreateView") - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + vm.displayUpArrow = parentFragmentManager.backStackEntryCount != 0 + if (savedInstanceState != null) vm.displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) val displayMetrics: DisplayMetrics = requireContext().resources.displayMetrics val screenWidthDp: Float = displayMetrics.widthPixels / displayMetrics.density - if (screenWidthDp > 600) numColumns = 6 + if (screenWidthDp > 600) vm.numColumns = 6 // Fill with dummy elements to have a fixed height and // prevent the UI elements below from jumping on slow connections - for (i in 0 until NUM_SUGGESTIONS) searchResult.add(PodcastSearchResult.dummy()) - val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } } + for (i in 0 until NUM_SUGGESTIONS) vm.searchResult.add(PodcastSearchResult.dummy()) + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { OnlineSearchScreen() } } } val PAFeed = realm.query(PAFeed::class).find() // for (p in directory) { @@ -138,16 +149,16 @@ class OnlineSearchFragment : Fragment() { // } Logd(TAG, "size of directory: ${PAFeed.size}") loadToplist() - if (isOPMLRestored && feedCount == 0) showOPMLRestoreDialog.value = true + if (isOPMLRestored && feedCount == 0) vm.showOPMLRestoreDialog.value = true return composeView } @Composable - fun MainView() { + fun OnlineSearchScreen() { val textColor = MaterialTheme.colorScheme.onSurface val actionColor = MaterialTheme.colorScheme.tertiary val scrollState = rememberScrollState() - ComfirmDialog(R.string.restore_subscriptions_label, stringResource(R.string.restore_subscriptions_summary), showOPMLRestoreDialog) { + ComfirmDialog(R.string.restore_subscriptions_label, stringResource(R.string.restore_subscriptions_summary), vm.showOPMLRestoreDialog) { performRestore(requireContext()) parentFragmentManager.popBackStack() } @@ -160,20 +171,20 @@ class OnlineSearchFragment : Fragment() { try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { e.printStackTrace() - mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) + vm.mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) } })) - Text(stringResource(R.string.search_vistaguide_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) })) - Text(stringResource(R.string.search_itunes_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) })) - Text(stringResource(R.string.search_fyyd_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) })) + Text(stringResource(R.string.search_vistaguide_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { vm.mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) })) + Text(stringResource(R.string.search_itunes_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { vm.mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) })) + Text(stringResource(R.string.search_fyyd_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { vm.mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) })) // Text(stringResource(R.string.gpodnet_search_hint), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) })) - Text(stringResource(R.string.search_podcastindex_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) })) - if (showOpmlImportSelectionDialog) OpmlImportSelectionDialog(readElements) { showOpmlImportSelectionDialog = false } + Text(stringResource(R.string.search_podcastindex_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { vm.mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) })) + if (vm.showOpmlImportSelectionDialog) OpmlImportSelectionDialog(vm.readElements) { vm.showOpmlImportSelectionDialog = false } Text(stringResource(R.string.opml_add_podcast_label), color = actionColor, modifier = Modifier.padding(start = 10.dp, top = 10.dp).clickable(onClick = { try { chooseOpmlImportPathLauncher.launch("*/*") } catch (e: ActivityNotFoundException) { e.printStackTrace() - mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) + vm.mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) } })) } @@ -186,9 +197,9 @@ class OnlineSearchFragment : Fragment() { TopAppBar(title = { SearchBarRow(R.string.search_podcast_hint) { queryText -> if (queryText.isBlank()) return@SearchBarRow if (queryText.matches("http[s]?://.*".toRegex())) addUrl(queryText) - else mainAct?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, queryText)) + else vm.mainAct?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, queryText)) } }, - navigationIcon = if (displayUpArrow) { + navigationIcon = if (vm.displayUpArrow) { { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } else { { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(Icons.Filled.Menu, contentDescription = "Open Drawer") } } @@ -203,7 +214,7 @@ class OnlineSearchFragment : Fragment() { override fun onStop() { super.onStop() - cancelFlowEvents() + vm.cancelFlowEvents() } @Composable @@ -218,39 +229,34 @@ class OnlineSearchFragment : Fragment() { Text(stringResource(R.string.discover_more), color = actionColor, modifier = Modifier.clickable(onClick = {(activity as MainActivity).loadChildFragment(DiscoveryFragment())})) } Box(modifier = Modifier.fillMaxWidth()) { - if (showGrid) NonlazyGrid(columns = numColumns, itemCount = searchResult.size, modifier = Modifier.fillMaxWidth()) { index -> - AsyncImage(model = ImageRequest.Builder(context).data(searchResult[index].imageUrl) + if (vm.showGrid) NonlazyGrid(columns = vm.numColumns, itemCount = vm.searchResult.size, modifier = Modifier.fillMaxWidth()) { index -> + AsyncImage(model = ImageRequest.Builder(context).data(vm.searchResult[index].imageUrl) .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), contentDescription = "imgvCover", modifier = Modifier.padding(top = 8.dp) .clickable(onClick = { Logd(TAG, "icon clicked!") - val podcast: PodcastSearchResult? = searchResult[index] + val podcast: PodcastSearchResult? = vm.searchResult[index] if (!podcast?.feedUrl.isNullOrEmpty()) { val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl) (activity as MainActivity).loadChildFragment(fragment) } })) } - if (showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { - Text(errorText, color = textColor) - if (showRetry) Button(onClick = { + if (vm.showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { + Text(vm.errorText, color = textColor) + if (vm.showRetry) Button(onClick = { prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply() loadToplist() - }) { Text(stringResource(retryTextRes)) } + }) { Text(stringResource(vm.retryTextRes)) } } } Text(stringResource(R.string.discover_powered_by_itunes), color = textColor, modifier = Modifier.align(Alignment.End)) } } - private var eventSink: Job? = null - private fun cancelFlowEvents() { - eventSink?.cancel() - eventSink = null - } private fun procFlowEvents() { - if (eventSink != null) return - eventSink = lifecycleScope.launch { + if (vm.eventSink != null) return + vm.eventSink = lifecycleScope.launch { EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { @@ -262,26 +268,26 @@ class OnlineSearchFragment : Fragment() { } private fun loadToplist() { - showError = false - showPowerBy = true - showRetry = false - retryTextRes = R.string.retry_label + vm.showError = false + vm.showPowerBy = true + vm.showRetry = false + vm.retryTextRes = R.string.retry_label val loader = ItunesTopListLoader(requireContext()) val countryCode: String = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!! if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) { - showError = true - errorText = requireContext().getString(R.string.discover_is_hidden) - showPowerBy = false - showRetry = false + vm.showError = true + vm.errorText = requireContext().getString(R.string.discover_is_hidden) + vm.showPowerBy = false + vm.showRetry = false return } if (BuildConfig.FLAVOR == "free" && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) == true) { - showError = true - errorText = "" - showGrid = true - showRetry = true - retryTextRes = R.string.discover_confirm - showPowerBy = true + vm.showError = true + vm.errorText = "" + vm.showGrid = true + vm.showRetry = true + vm.retryTextRes = R.string.discover_confirm + vm.showPowerBy = true return } @@ -289,29 +295,29 @@ class OnlineSearchFragment : Fragment() { try { val searchResults_ = withContext(Dispatchers.IO) { loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList()) } withContext(Dispatchers.Main) { - showError = false + vm.showError = false if (searchResults_.isEmpty()) { - errorText = requireContext().getString(R.string.search_status_no_results) - showError = true - showGrid = false + vm.errorText = requireContext().getString(R.string.search_status_no_results) + vm.showError = true + vm.showGrid = false } else { - showGrid = true - searchResult.clear() - searchResult.addAll(searchResults_) + vm.showGrid = true + vm.searchResult.clear() + vm.searchResult.addAll(searchResults_) } } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) - showError = true - showGrid = false - showRetry = true - errorText = e.localizedMessage ?: "" + vm.showError = true + vm.showGrid = false + vm.showRetry = true + vm.errorText = e.localizedMessage ?: "" } } } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) + outState.putBoolean(KEY_UP_ARROW, vm.displayUpArrow) super.onSaveInstanceState(outState) } @@ -335,7 +341,7 @@ class OnlineSearchFragment : Fragment() { private fun addUrl(url: String) { val fragment: Fragment = OnlineFeedFragment.newInstance(url) - mainAct?.loadChildFragment(fragment) + vm.mainAct?.loadChildFragment(fragment) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt index 09435c6a..4202e6d2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt @@ -155,67 +155,66 @@ class QueuesFragment : Fragment() { lifecycle.addObserver(vm.swipeActions) lifecycle.addObserver(vm.swipeActionsBin) - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(if (vm.showBin) vm.swipeActionsBin else vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> - vm.swipeActions.actions = actions - refreshSwipeTelltale() + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { QueuesScreen() } } } + + lifecycle.addObserver(vm.swipeActions) + refreshSwipeTelltale() + return composeView + } + + @Composable + fun QueuesScreen() { + if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(if (vm.showBin) vm.swipeActionsBin else vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> + vm.swipeActions.actions = actions + refreshSwipeTelltale() + } + ComfirmDialog(titleRes = R.string.clear_queue_label, message = stringResource(R.string.clear_queue_confirmation_msg), showDialog = vm.showClearQueueDialog) { clearQueue() } + if (vm.shouldShowLockWarningDiwload) ShowLockWarning { vm.shouldShowLockWarningDiwload = false } + RenameQueueDialog(showDialog = vm.showRenameQueueDialog.value, onDismiss = { vm.showRenameQueueDialog.value = false }) + AddQueueDialog(showDialog = vm.showAddQueueDialog.value, onDismiss = { vm.showAddQueueDialog.value = false }) + vm.swipeActions.ActionOptionsDialog() + vm.swipeActionsBin.ActionOptionsDialog() + + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + if (vm.showBin) { + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + InforBar(vm.infoBarText, leftAction = vm.leftActionStateBin, rightAction = vm.rightActionStateBin, actionConfig = { vm.showSwipeActionsDialog = true }) + val leftCB = { episode: Episode -> + if (vm.leftActionStateBin.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionStateBin.value.performAction(episode) } - ComfirmDialog(titleRes = R.string.clear_queue_label, message = stringResource(R.string.clear_queue_confirmation_msg), showDialog = vm.showClearQueueDialog) { clearQueue() } - if (vm.shouldShowLockWarningDiwload) ShowLockWarning { vm.shouldShowLockWarningDiwload = false } - RenameQueueDialog(showDialog = vm.showRenameQueueDialog.value, onDismiss = { vm.showRenameQueueDialog.value = false }) - AddQueueDialog(showDialog = vm.showAddQueueDialog.value, onDismiss = { vm.showAddQueueDialog.value = false }) - vm.swipeActions.ActionOptionsDialog() - vm.swipeActionsBin.ActionOptionsDialog() - - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - if (vm.showBin) { - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - InforBar(vm.infoBarText, leftAction = vm.leftActionStateBin, rightAction = vm.rightActionStateBin, actionConfig = { vm.showSwipeActionsDialog = true }) - val leftCB = { episode: Episode -> - if (vm.leftActionStateBin.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.leftActionStateBin.value.performAction(episode) - } - val rightCB = { episode: Episode -> - if (vm.rightActionStateBin.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.rightActionStateBin.value.performAction(episode) - } - EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) - } - } else { - if (vm.showFeeds) Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) { FeedsGrid() } - else { - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - if (vm.showSortDialog) EpisodeSortDialog(initOrder = vm.sortOrder, showKeepSorted = true, onDismissRequest = { vm.showSortDialog = false }) { sortOrder, keep -> - if (sortOrder != EpisodeSortOrder.RANDOM && sortOrder != EpisodeSortOrder.RANDOM1) isQueueKeepSorted = keep - queueKeepSortedOrder = sortOrder - reorderQueue(sortOrder, true) - } - - InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) - val leftCB = { episode: Episode -> - if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.leftActionState.value.performAction(episode) - } - val rightCB = { episode: Episode -> - if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true - else vm.rightActionState.value.performAction(episode) - } - EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, - isDraggable = vm.dragDropEnabled, dragCB = { iFrom, iTo -> runOnIOScope { moveInQueueSync(iFrom, iTo, true) } }, - leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) - } - } + val rightCB = { episode: Episode -> + if (vm.rightActionStateBin.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionStateBin.value.performAction(episode) + } + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) + } + } else { + if (vm.showFeeds) Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) { FeedsGrid() } + else { + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + if (vm.showSortDialog) EpisodeSortDialog(initOrder = vm.sortOrder, showKeepSorted = true, onDismissRequest = { vm.showSortDialog = false }) { sortOrder, keep -> + if (sortOrder != EpisodeSortOrder.RANDOM && sortOrder != EpisodeSortOrder.RANDOM1) isQueueKeepSorted = keep + queueKeepSortedOrder = sortOrder + reorderQueue(sortOrder, true) + } + + InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) + val leftCB = { episode: Episode -> + if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionState.value.performAction(episode) } + val rightCB = { episode: Episode -> + if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionState.value.performAction(episode) + } + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, + isDraggable = vm.dragDropEnabled, dragCB = { iFrom, iTo -> runOnIOScope { moveInQueueSync(iFrom, iTo, true) } }, + leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) } } } } - - lifecycle.addObserver(vm.swipeActions) - refreshSwipeTelltale() - return composeView } override fun onStart() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index f582138a..815c3530 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -66,95 +66,99 @@ import java.text.NumberFormat import kotlin.math.min class SearchFragment : Fragment() { - private lateinit var automaticSearchDebouncer: Handler - private val pafeeds = mutableStateListOf() + class SearchVM { + internal lateinit var automaticSearchDebouncer: Handler - private val feeds = mutableStateListOf() - private val episodes = mutableListOf() - private val vms = mutableStateListOf() - private var infoBarText = mutableStateOf("") - private var searchInFeed by mutableStateOf(false) - private var feedName by mutableStateOf("") - private var queryText by mutableStateOf("") + internal val pafeeds = mutableStateListOf() - private var showSwipeActionsDialog by mutableStateOf(false) - private lateinit var swipeActions: SwipeActions - private var leftActionState = mutableStateOf(NoActionSwipeAction()) - private var rightActionState = mutableStateOf(NoActionSwipeAction()) + internal val feeds = mutableStateListOf() + internal val episodes = mutableListOf() + internal val vms = mutableStateListOf() + internal var infoBarText = mutableStateOf("") + internal var searchInFeed by mutableStateOf(false) + internal var feedName by mutableStateOf("") + internal var queryText by mutableStateOf("") + + internal var showSwipeActionsDialog by mutableStateOf(false) + internal lateinit var swipeActions: SwipeActions + internal var leftActionState = mutableStateOf(NoActionSwipeAction()) + internal var rightActionState = mutableStateOf(NoActionSwipeAction()) + } + + private val vm = SearchVM() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true - automaticSearchDebouncer = Handler(Looper.getMainLooper()) + vm.automaticSearchDebouncer = Handler(Looper.getMainLooper()) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - swipeActions = SwipeActions(this, TAG) - leftActionState.value = swipeActions.actions.left[0] - rightActionState.value = swipeActions.actions.right[0] - lifecycle.addObserver(swipeActions) + vm.swipeActions = SwipeActions(this, TAG) + vm.leftActionState.value = vm.swipeActions.actions.left[0] + vm.rightActionState.value = vm.swipeActions.actions.right[0] + lifecycle.addObserver(vm.swipeActions) if (requireArguments().getLong(ARG_FEED, 0) > 0L) { - searchInFeed = true - feedName = requireArguments().getString(ARG_FEED_NAME, "") + vm.searchInFeed = true + vm.feedName = requireArguments().getString(ARG_FEED_NAME, "") + } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { SearchScreen() } } } + refreshSwipeTelltale() + val arg = requireArguments().getString(ARG_QUERY, "") + if (arg.isNotBlank()) search(arg) + return composeView + } + + @Composable + fun SearchScreen() { + if (vm.showSwipeActionsDialog) SwipeActionsSettingDialog(vm.swipeActions, onDismissRequest = { vm.showSwipeActionsDialog = false }) { actions -> + vm.swipeActions.actions = actions + refreshSwipeTelltale() } - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (showSwipeActionsDialog) SwipeActionsSettingDialog(swipeActions, onDismissRequest = { showSwipeActionsDialog = false }) { actions -> - swipeActions.actions = actions - refreshSwipeTelltale() + vm.swipeActions.ActionOptionsDialog() + val tabTitles = listOf(R.string.episodes_label, R.string.feeds, R.string.pafeeds) + val tabCounts = listOf(vm.episodes.size, vm.feeds.size, vm.pafeeds.size) + val selectedTabIndex = remember { mutableIntStateOf(0) } + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + if (vm.searchInFeed) FilterChip(onClick = { }, label = { Text(vm.feedName) }, selected = vm.searchInFeed, + trailingIcon = { + Icon(imageVector = Icons.Filled.Close, contentDescription = "Close icon", modifier = Modifier.size(FilterChipDefaults.IconSize).clickable(onClick = { + requireArguments().putLong(ARG_FEED, 0) + vm.searchInFeed = false + })) } - swipeActions.ActionOptionsDialog() - val tabTitles = listOf(R.string.episodes_label, R.string.feeds, R.string.pafeeds) - val tabCounts = listOf(episodes.size, feeds.size, pafeeds.size) - val selectedTabIndex = remember { mutableIntStateOf(0) } - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - if (searchInFeed) FilterChip(onClick = { }, label = { Text(feedName) }, selected = searchInFeed, - trailingIcon = { - Icon(imageVector = Icons.Filled.Close, contentDescription = "Close icon", modifier = Modifier.size(FilterChipDefaults.IconSize).clickable(onClick = { - requireArguments().putLong(ARG_FEED, 0) - searchInFeed = false - })) - } - ) - CriteriaList() - TabRow(modifier = Modifier.fillMaxWidth(), selectedTabIndex = selectedTabIndex.value, divider = {}, indicator = { tabPositions -> - Box(modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex.value]).height(4.dp).background(Color.Blue)) - }) { - tabTitles.forEachIndexed { index, titleRes -> - Tab(text = { Text(stringResource(titleRes)+"(${tabCounts[index]})") }, selected = selectedTabIndex.value == index, onClick = { selectedTabIndex.value = index }) - } - } - when (selectedTabIndex.value) { - 0 -> { - InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { showSwipeActionsDialog = true }) - EpisodeLazyColumn(activity as MainActivity, vms = vms, buildMoreItems = { buildMoreItems() }, - leftSwipeCB = { - if (leftActionState.value is NoActionSwipeAction) showSwipeActionsDialog = true - else leftActionState.value.performAction(it) - }, - rightSwipeCB = { - if (rightActionState.value is NoActionSwipeAction) showSwipeActionsDialog = true - else rightActionState.value.performAction(it) - }, - ) - } - 1 -> FeedsColumn() - 2 -> PAFeedsColumn() - } - } + ) + CriteriaList() + TabRow(modifier = Modifier.fillMaxWidth(), selectedTabIndex = selectedTabIndex.value, divider = {}, indicator = { tabPositions -> + Box(modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex.value]).height(4.dp).background(Color.Blue)) + }) { + tabTitles.forEachIndexed { index, titleRes -> + Tab(text = { Text(stringResource(titleRes)+"(${tabCounts[index]})") }, selected = selectedTabIndex.value == index, onClick = { selectedTabIndex.value = index }) + } + } + when (selectedTabIndex.value) { + 0 -> { + InforBar(vm.infoBarText, leftAction = vm.leftActionState, rightAction = vm.rightActionState, actionConfig = { vm.showSwipeActionsDialog = true }) + EpisodeLazyColumn(activity as MainActivity, vms = vm.vms, buildMoreItems = { buildMoreItems() }, + leftSwipeCB = { + if (vm.leftActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.leftActionState.value.performAction(it) + }, + rightSwipeCB = { + if (vm.rightActionState.value is NoActionSwipeAction) vm.showSwipeActionsDialog = true + else vm.rightActionState.value.performAction(it) + }, + ) } + 1 -> FeedsColumn() + 2 -> PAFeedsColumn() } } } - refreshSwipeTelltale() - val arg = requireArguments().getString(ARG_QUERY, "") - if (arg.isNotBlank()) search(arg) - return composeView } override fun onStart() { @@ -169,29 +173,29 @@ class SearchFragment : Fragment() { override fun onDestroyView() { Logd(TAG, "onDestroyView") - episodes.clear() - feeds.clear() - stopMonitor(vms) - vms.clear() + vm.episodes.clear() + vm.feeds.clear() + stopMonitor(vm.vms) + vm.vms.clear() super.onDestroyView() } fun buildMoreItems() { - val nextItems = (vms.size until min(vms.size + VMS_CHUNK_SIZE, episodes.size)).map { EpisodeVM(episodes[it], TAG) } - if (nextItems.isNotEmpty()) vms.addAll(nextItems) + val nextItems = (vm.vms.size until min(vm.vms.size + VMS_CHUNK_SIZE, vm.episodes.size)).map { EpisodeVM(vm.episodes[it], TAG) } + if (nextItems.isNotEmpty()) vm.vms.addAll(nextItems) } private fun refreshSwipeTelltale() { - leftActionState.value = swipeActions.actions.left[0] - rightActionState.value = swipeActions.actions.right[0] + vm.leftActionState.value = vm.swipeActions.actions.left[0] + vm.rightActionState.value = vm.swipeActions.actions.right[0] } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyTopAppBar() { TopAppBar(title = { SearchBarRow(R.string.search_label) { - queryText = it - search(queryText) + vm.queryText = it + search(vm.queryText) }}, navigationIcon = { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } ) @@ -210,7 +214,7 @@ class SearchFragment : Fragment() { EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { - is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent -> search(queryText) + is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent -> search(vm.queryText) // is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() else -> {} } @@ -229,8 +233,8 @@ class SearchFragment : Fragment() { private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) { for (url in event.urls) { - val pos: Int = Episodes.indexOfItemWithDownloadUrl(episodes, url) - if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal + val pos: Int = Episodes.indexOfItemWithDownloadUrl(vm.episodes, url) + if (pos >= 0) vm.vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal } } @@ -242,8 +246,8 @@ class SearchFragment : Fragment() { if (query.isBlank()) return if (searchJob != null) { searchJob?.cancel() - stopMonitor(vms) - vms.clear() + stopMonitor(vm.vms) + vm.vms.clear() } searchJob = lifecycleScope.launch { try { @@ -260,20 +264,20 @@ class SearchFragment : Fragment() { } withContext(Dispatchers.Main) { val first_ = results_.episodes - episodes.clear() - stopMonitor(vms) - vms.clear() + vm.episodes.clear() + stopMonitor(vm.vms) + vm.vms.clear() if (first_.isNotEmpty()) { - episodes.addAll(first_) + vm.episodes.addAll(first_) buildMoreItems() } - infoBarText.value = "${episodes.size} episodes" + vm.infoBarText.value = "${vm.episodes.size} episodes" if (requireArguments().getLong(ARG_FEED, 0) == 0L) { - feeds.clear() - if (results_.feeds.isNotEmpty()) feeds.addAll(results_.feeds) - } else feeds.clear() - pafeeds.clear() - if (results_.pafeeds.isNotEmpty()) pafeeds.addAll(results_.pafeeds) + vm.feeds.clear() + if (results_.feeds.isNotEmpty()) vm.feeds.addAll(results_.feeds) + } else vm.feeds.clear() + vm.pafeeds.clear() + if (results_.pafeeds.isNotEmpty()) vm.pafeeds.addAll(results_.pafeeds) } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } }.apply { invokeOnCompletion { searchJob = null } } @@ -319,7 +323,7 @@ class SearchFragment : Fragment() { val context = LocalContext.current val lazyListState = rememberLazyListState() LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(feeds, key = { _, feed -> feed.id }) { index, feed -> + itemsIndexed(vm.feeds, key = { _, feed -> feed.id }) { index, feed -> Row(Modifier.background(MaterialTheme.colorScheme.surface)) { val imgLoc = remember(feed) { feed.imageUrl } AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) @@ -368,7 +372,7 @@ class SearchFragment : Fragment() { val context = LocalContext.current val lazyListState = rememberLazyListState() LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(pafeeds, key = { _, feed -> feed.id }) { index, feed -> + itemsIndexed(vm.pafeeds, key = { _, feed -> feed.id }) { index, feed -> Row(verticalAlignment = Alignment.CenterVertically) { val imgLoc = remember(feed) { feed.imageUrl } AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) @@ -523,7 +527,7 @@ class SearchFragment : Fragment() { // } private fun searchOnline() { - val query = queryText + val query = vm.queryText if (query.matches("http[s]?://.*".toRegex())) { val fragment: Fragment = OnlineFeedFragment.newInstance(query) (activity as MainActivity).loadChildFragment(fragment) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt index 48388f50..94f00f57 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt @@ -42,66 +42,71 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class SearchResultsFragment : Fragment() { - private var searchProvider: PodcastSearcher? = null - private val feedLogs = getFeedLogMap() + class SearchResultsVM { + internal var searchProvider: PodcastSearcher? = null - private var defaultText by mutableStateOf("") - private var searchResults = mutableStateListOf() - private var errorText by mutableStateOf("") - private var retryQerry by mutableStateOf("") - private var showProgress by mutableStateOf(true) - private var noResultText by mutableStateOf("") + internal val feedLogs = getFeedLogMap() + + internal var defaultText by mutableStateOf("") + internal var searchResults = mutableStateListOf() + internal var errorText by mutableStateOf("") + internal var retryQerry by mutableStateOf("") + internal var showProgress by mutableStateOf(true) + internal var noResultText by mutableStateOf("") + } + + private val vm = SearchResultsVM() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) for (info in PodcastSearcherRegistry.searchProviders) { Logd(TAG, "searchProvider: $info") if (info.searcher.javaClass.getName() == requireArguments().getString(ARG_SEARCHER)) { - searchProvider = info.searcher + vm.searchProvider = info.searcher break } } - if (searchProvider == null) Logd(TAG,"Podcast searcher not found") - defaultText = requireArguments().getString(ARG_QUERY, "") - search(defaultText) + if (vm.searchProvider == null) Logd(TAG,"Podcast searcher not found") + vm.defaultText = requireArguments().getString(ARG_QUERY, "") + search(vm.defaultText) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { SearchResultsScreen() } } } return composeView } @Composable - fun MainView() { + fun SearchResultsScreen() { val textColor = MaterialTheme.colorScheme.onSurface Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> ConstraintLayout(modifier = Modifier.padding(innerPadding).fillMaxSize()) { val (gridView, progressBar, empty, txtvError, butRetry, powered) = createRefs() - if (showProgress) CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) }) + if (vm.showProgress) CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) }) val lazyListState = rememberLazyListState() - if (searchResults.isNotEmpty()) LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) + if (vm.searchResults.isNotEmpty()) LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) .constrainAs(gridView) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) start.linkTo(parent.start) }, verticalArrangement = Arrangement.spacedBy(8.dp)) { - items(searchResults.size) { index -> - val result = searchResults[index] + items(vm.searchResults.size) { index -> + val result = vm.searchResults[index] val urlPrepared by remember { mutableStateOf(prepareUrl(result.feedUrl!!)) } - val sLog = remember { mutableStateOf(feedLogs[urlPrepared]) } + val sLog = remember { mutableStateOf(vm.feedLogs[urlPrepared]) } // Logd(TAG, "result: ${result.feedUrl} ${feedLogs[urlPrepared]}") OnlineFeedItem(activity = activity as MainActivity, result, sLog.value) } } - if (searchResults.isEmpty()) Text(noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) - if (errorText.isNotEmpty()) Text(errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) }) - if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom) }, onClick = { search(retryQerry) }) { + if (vm.searchResults.isEmpty()) Text(vm.noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) + if (vm.errorText.isNotEmpty()) Text(vm.errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) }) + if (vm.retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom) }, onClick = { search(vm.retryQerry) }) { Text(stringResource(id = R.string.retry_label)) } - Text(getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(Color.LightGray) + Text(getString(R.string.search_powered_by, vm.searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(Color.LightGray) .constrainAs(powered) { bottom.linkTo(parent.bottom) end.linkTo(parent.end) @@ -119,7 +124,7 @@ class SearchResultsFragment : Fragment() { } override fun onDestroy() { - searchResults.clear() + vm.searchResults.clear() super.onDestroy() } @@ -129,7 +134,7 @@ class SearchResultsFragment : Fragment() { if (query.isBlank()) return if (searchJob != null) { searchJob?.cancel() - searchResults.clear() + vm.searchResults.clear() } showOnlyProgressBar() searchJob = lifecycleScope.launch(Dispatchers.IO) { @@ -139,13 +144,13 @@ class SearchResultsFragment : Fragment() { return 0L } try { - val result = searchProvider?.search(query) ?: listOf() + val result = vm.searchProvider?.search(query) ?: listOf() for (r in result) r.feedId = feedId(r) - searchResults.clear() - searchResults.addAll(result) + vm.searchResults.clear() + vm.searchResults.addAll(result) withContext(Dispatchers.Main) { - showProgress = false - noResultText = getString(R.string.no_results_for_query, query) + vm.showProgress = false + vm.noResultText = getString(R.string.no_results_for_query, query) } } catch (e: Exception) { handleSearchError(e, query) } }.apply { invokeOnCompletion { searchJob = null } } @@ -153,15 +158,15 @@ class SearchResultsFragment : Fragment() { private fun handleSearchError(e: Throwable, query: String) { Logd(TAG, "exception: ${e.message}") - showProgress = false - errorText = e.toString() - retryQerry = query + vm.showProgress = false + vm.errorText = e.toString() + vm.retryQerry = query } private fun showOnlyProgressBar() { - errorText = "" - retryQerry = "" - showProgress = true + vm.errorText = "" + vm.retryQerry = "" + vm.showProgress = true } // private fun showInputMethod(view: View) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt index 8188948e..2d848858 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt @@ -62,71 +62,74 @@ import kotlin.math.min class StatisticsFragment : Fragment() { val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) } - private var includeMarkedAsPlayed by mutableStateOf(false) - private var statisticsState by mutableIntStateOf(0) - private val selectedTabIndex = mutableIntStateOf(0) - private var showFilter by mutableStateOf(false) + class StatisticsVM { + internal var includeMarkedAsPlayed by mutableStateOf(false) + internal var statisticsState by mutableIntStateOf(0) + internal val selectedTabIndex = mutableIntStateOf(0) + internal var showFilter by mutableStateOf(false) - lateinit var statsResult: StatisticsResult + lateinit var statsResult: StatisticsResult - private val showResetDialog = mutableStateOf(false) + internal val showResetDialog = mutableStateOf(false) + } + + private val vm = StatisticsVM() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - ComfirmDialog(titleRes = R.string.statistics_reset_data, message = stringResource(R.string.statistics_reset_data_msg), showDialog = showResetDialog) { - prefs.edit()?.putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)?.putLong(PREF_FILTER_FROM, 0)?.putLong(PREF_FILTER_TO, Long.MAX_VALUE)?.apply() - CoroutineScope(Dispatchers.IO).launch { - try { - while (realm.query(Episode::class).query("playedDuration != 0 || timeSpent != 0").count().find() > 0) { - realm.write { - var mediaAll = query(Episode::class).query("playedDuration != 0 || timeSpent != 0").find() - if (mediaAll.isNotEmpty()) { - Logd(TAG, "mediaAll: ${mediaAll.size}") - for (m in mediaAll) { - Logd(TAG, "m: ${m.title}") - m.playedDuration = 0 - m.timeSpent = 0 - m.timeSpentOnStart = 0 - m.startTime = 0 - } - } - } - } - statisticsState++ - } catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) } - } - } - if (showFilter) DatesFilterDialogCompose(inclPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false), from = prefs.getLong(PREF_FILTER_FROM, 0), to = prefs.getLong(PREF_FILTER_TO, Long.MAX_VALUE), - oldestDate = statsResult.oldestDate, onDismissRequest = {showFilter = false} ) { timeFilterFrom, timeFilterTo, includeMarkedAsPlayed_ -> - prefs.edit()?.putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed_)?.putLong(PREF_FILTER_FROM, timeFilterFrom)?.putLong(PREF_FILTER_TO, timeFilterTo)?.apply() - includeMarkedAsPlayed = includeMarkedAsPlayed_ - statisticsState++ - } - val tabTitles = listOf(R.string.subscriptions_label, R.string.months_statistics_label, R.string.downloads_label) - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - TabRow(modifier = Modifier.fillMaxWidth(), selectedTabIndex = selectedTabIndex.value, divider = {}, indicator = { tabPositions -> - Box(modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex.value]).height(4.dp).background(Color.Blue)) - }) { - tabTitles.forEachIndexed { index, titleRes -> - Tab(text = { Text(stringResource(titleRes)) }, selected = selectedTabIndex.value == index, onClick = { selectedTabIndex.value = index }) + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { StatisticsScreen() } } } + return composeView + } + + @Composable + fun StatisticsScreen() { + ComfirmDialog(titleRes = R.string.statistics_reset_data, message = stringResource(R.string.statistics_reset_data_msg), showDialog = vm.showResetDialog) { + prefs.edit()?.putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)?.putLong(PREF_FILTER_FROM, 0)?.putLong(PREF_FILTER_TO, Long.MAX_VALUE)?.apply() + CoroutineScope(Dispatchers.IO).launch { + try { + while (realm.query(Episode::class).query("playedDuration != 0 || timeSpent != 0").count().find() > 0) { + realm.write { + var mediaAll = query(Episode::class).query("playedDuration != 0 || timeSpent != 0").find() + if (mediaAll.isNotEmpty()) { + Logd(TAG, "mediaAll: ${mediaAll.size}") + for (m in mediaAll) { + Logd(TAG, "m: ${m.title}") + m.playedDuration = 0 + m.timeSpent = 0 + m.timeSpentOnStart = 0 + m.startTime = 0 } } - when (selectedTabIndex.value) { - 0 -> PlayedTime() - 1 -> MonthlyStats() - 2 -> DownloadStats() - } } } + vm.statisticsState++ + } catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) } + } + } + if (vm.showFilter) DatesFilterDialogCompose(inclPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false), from = prefs.getLong(PREF_FILTER_FROM, 0), to = prefs.getLong(PREF_FILTER_TO, Long.MAX_VALUE), + oldestDate = vm.statsResult.oldestDate, onDismissRequest = {vm.showFilter = false} ) { timeFilterFrom, timeFilterTo, includeMarkedAsPlayed_ -> + prefs.edit()?.putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed_)?.putLong(PREF_FILTER_FROM, timeFilterFrom)?.putLong(PREF_FILTER_TO, timeFilterTo)?.apply() + vm.includeMarkedAsPlayed = includeMarkedAsPlayed_ + vm.statisticsState++ + } + val tabTitles = listOf(R.string.subscriptions_label, R.string.months_statistics_label, R.string.downloads_label) + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + TabRow(modifier = Modifier.fillMaxWidth(), selectedTabIndex = vm.selectedTabIndex.value, divider = {}, indicator = { tabPositions -> + Box(modifier = Modifier.tabIndicatorOffset(tabPositions[vm.selectedTabIndex.value]).height(4.dp).background(Color.Blue)) + }) { + tabTitles.forEachIndexed { index, titleRes -> + Tab(text = { Text(stringResource(titleRes)) }, selected = vm.selectedTabIndex.value == index, onClick = { vm.selectedTabIndex.value = index }) + } + } + when (vm.selectedTabIndex.value) { + 0 -> PlayedTime() + 1 -> MonthlyStats() + 2 -> DownloadStats() } } } - return composeView } @OptIn(ExperimentalMaterial3Api::class) @@ -136,12 +139,12 @@ class StatisticsFragment : Fragment() { TopAppBar(title = { Text(stringResource(R.string.statistics_label)) }, navigationIcon = { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chart_box), contentDescription = "Open Drawer") } }, actions = { - if (selectedTabIndex.value == 0) IconButton(onClick = { showFilter = true + if (vm.selectedTabIndex.value == 0) IconButton(onClick = { vm.showFilter = true }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_filter), contentDescription = "filter") } IconButton(onClick = { expanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Menu") } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - if (selectedTabIndex.value == 0 || selectedTabIndex.value == 1) DropdownMenuItem(text = { Text(stringResource(R.string.statistics_reset_data)) }, onClick = { - showResetDialog.value = true + if (vm.selectedTabIndex.value == 0 || vm.selectedTabIndex.value == 1) DropdownMenuItem(text = { Text(stringResource(R.string.statistics_reset_data)) }, onClick = { + vm.showResetDialog.value = true expanded = false }) } @@ -160,13 +163,13 @@ class StatisticsFragment : Fragment() { var timeSpentToday = 0L fun setTimeFilter(includeMarkedAsPlayed_: Boolean, timeFilterFrom_: Long, timeFilterTo_: Long) { - includeMarkedAsPlayed = includeMarkedAsPlayed_ + vm.includeMarkedAsPlayed = includeMarkedAsPlayed_ timeFilterFrom = timeFilterFrom_ timeFilterTo = timeFilterTo_ } fun loadStatistics() { - includeMarkedAsPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false) - val statsToday = getStatistics(includeMarkedAsPlayed, LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(), Long.MAX_VALUE) + vm.includeMarkedAsPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false) + val statsToday = getStatistics(vm.includeMarkedAsPlayed, LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(), Long.MAX_VALUE) for (item in statsToday.statsItems) { timePlayedToday += item.timePlayed timeSpentToday += item.timeSpent @@ -174,22 +177,22 @@ class StatisticsFragment : Fragment() { timeFilterFrom = prefs.getLong(PREF_FILTER_FROM, 0) timeFilterTo = prefs.getLong(PREF_FILTER_TO, Long.MAX_VALUE) try { - statsResult = getStatistics(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo) - statsResult.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem -> item2.timePlayed.compareTo(item1.timePlayed) } - val dataValues = MutableList(statsResult.statsItems.size){0f} - for (i in statsResult.statsItems.indices) { - val item = statsResult.statsItems[i] + vm.statsResult = getStatistics(vm.includeMarkedAsPlayed, timeFilterFrom, timeFilterTo) + vm.statsResult.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem -> item2.timePlayed.compareTo(item1.timePlayed) } + val dataValues = MutableList(vm.statsResult.statsItems.size){0f} + for (i in vm.statsResult.statsItems.indices) { + val item = vm.statsResult.statsItems[i] dataValues[i] = item.timePlayed.toFloat() timeSpentSum += item.timeSpent } chartData = LineChartData(dataValues) // When "from" is "today", set it to today - setTimeFilter(includeMarkedAsPlayed, - max(min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), statsResult.oldestDate.toDouble()).toLong(), + setTimeFilter(vm.includeMarkedAsPlayed, + max(min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), vm.statsResult.oldestDate.toDouble()).toLong(), min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong()) } catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) } } - if (statisticsState >= 0) loadStatistics() + if (vm.statisticsState >= 0) loadStatistics() Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface) @@ -198,7 +201,7 @@ class StatisticsFragment : Fragment() { Spacer(Modifier.width(20.dp)) Text( stringResource(R.string.spent) + ": " + getDurationStringShort(timeSpentToday.toInt()*1000, true), color = MaterialTheme.colorScheme.onSurface) } - val headerCaption = if (includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total) + val headerCaption = if (vm.includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total) else { if (timeFilterFrom != 0L || timeFilterTo != Long.MAX_VALUE) { val skeleton = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM yyyy") @@ -216,7 +219,7 @@ class StatisticsFragment : Fragment() { Text( stringResource(R.string.spent) + ": " + durationInHours(timeSpentSum), color = MaterialTheme.colorScheme.onSurface) } HorizontalLineChart(chartData) - StatsList(statsResult, chartData) { item -> + StatsList(vm.statsResult, chartData) { item -> context.getString(R.string.duration) + ": " + durationInHours(item.timePlayed) + " \t " + context.getString(R.string.spent) + ": " + durationInHours(item.timeSpent) } } @@ -229,7 +232,7 @@ class StatisticsFragment : Fragment() { fun loadMonthlyStatistics() { try { - includeMarkedAsPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false) + vm.includeMarkedAsPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false) val months: MutableList = ArrayList() val medias = realm.query(Episode::class).query("lastPlayedTime > 0").find() val groupdMedias = medias.groupBy { @@ -253,7 +256,7 @@ class StatisticsFragment : Fragment() { for (m in medias_) { dur += if (m.playedDuration > 0) m.playedDuration else { - if (includeMarkedAsPlayed) { + if (vm.includeMarkedAsPlayed) { if (m.playbackCompletionTime > 0 || m.playState >= PlayState.SKIPPED.code) m.duration else if (m.position > 0) m.position else 0 } else m.position @@ -303,7 +306,7 @@ class StatisticsFragment : Fragment() { } } } - if (statisticsState >= 0) loadMonthlyStatistics() + if (vm.statisticsState >= 0) loadMonthlyStatistics() Column { Row(modifier = Modifier.horizontalScroll(rememberScrollState()).padding(start = 20.dp, end = 20.dp)) { BarChart() } Spacer(Modifier.height(20.dp)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 295fe427..11cbcb3b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -2,13 +2,12 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.net.feed.FeedUpdateManager +import ac.mdiq.podcini.preferences.AppPreferences.AppPrefs +import ac.mdiq.podcini.preferences.AppPreferences.getPref import ac.mdiq.podcini.preferences.DocumentFileExportWorker import ac.mdiq.podcini.preferences.ExportTypes import ac.mdiq.podcini.preferences.ExportWorker import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter -import ac.mdiq.podcini.preferences.AppPreferences -import ac.mdiq.podcini.preferences.AppPreferences.AppPrefs -import ac.mdiq.podcini.preferences.AppPreferences.getPref import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.RealmDB.realm @@ -91,9 +90,6 @@ import java.util.* class SubscriptionsFragment : Fragment() { val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences("SubscriptionsFragmentPrefs", Context.MODE_PRIVATE) } - private val tags: MutableList = mutableListOf() - private val queueIds: MutableList = mutableListOf() - private var _feedsFilter: String? = null private var feedsFilter: String get() { @@ -126,42 +122,47 @@ class SubscriptionsFragment : Fragment() { prefs.edit().putInt("queueFilterIndex", index).apply() } - private var infoTextFiltered = "" - private var infoTextUpdate = "" + class SubscriptionsVM { + internal val tags: MutableList = mutableListOf() + internal val queueIds: MutableList = mutableListOf() + internal val spinnerTexts: MutableList = mutableListOf("Any queue", "No queue") - // TODO: currently not used - private var displayedFolder by mutableStateOf("") - private var displayUpArrow = false + internal var infoTextFiltered = "" + internal var infoTextUpdate = "" - private var txtvInformation by mutableStateOf("") - private var feedCount by mutableStateOf("") - private var feedSorted by mutableIntStateOf(0) + // TODO: currently not used + internal var displayedFolder by mutableStateOf("") + internal var displayUpArrow = false - private var sortIndex by mutableStateOf(0) - private var titleAscending by mutableStateOf(true) - private var dateAscending by mutableStateOf(true) - private var countAscending by mutableStateOf(true) - private var dateSortIndex by mutableStateOf(0) - private val playStateSort = MutableList(PlayState.entries.size) { mutableStateOf(false)} - private val playStateCodeSet = mutableSetOf() - private val ratingSort = MutableList(Rating.entries.size) { mutableStateOf(false)} - private val ratingCodeSet = mutableSetOf() - private var downlaodedSortIndex by mutableStateOf(-1) - private var commentedSortIndex by mutableStateOf(-1) + internal var txtvInformation by mutableStateOf("") + internal var feedCount by mutableStateOf("") + internal var feedSorted by mutableIntStateOf(0) - private var feedListFiltered = mutableStateListOf() - private var showFilterDialog by mutableStateOf(false) - private var showSortDialog by mutableStateOf(false) - private var noSubscription by mutableStateOf(false) - private var showNewSynthetic by mutableStateOf(false) + internal var sortIndex by mutableStateOf(0) + internal var titleAscending by mutableStateOf(true) + internal var dateAscending by mutableStateOf(true) + internal var countAscending by mutableStateOf(true) + internal var dateSortIndex by mutableStateOf(0) + internal val playStateSort = MutableList(PlayState.entries.size) { mutableStateOf(false) } + internal val playStateCodeSet = mutableSetOf() + internal val ratingSort = MutableList(Rating.entries.size) { mutableStateOf(false) } + internal val ratingCodeSet = mutableSetOf() + internal var downlaodedSortIndex by mutableStateOf(-1) + internal var commentedSortIndex by mutableStateOf(-1) - private var useGrid by mutableStateOf(null) - private val useGridLayout by mutableStateOf(getPref(AppPrefs.prefFeedGridLayout, false)) + internal var feedListFiltered = mutableStateListOf() + internal var showFilterDialog by mutableStateOf(false) + internal var showSortDialog by mutableStateOf(false) + internal var noSubscription by mutableStateOf(false) + internal var showNewSynthetic by mutableStateOf(false) - private var selectMode by mutableStateOf(false) + internal var useGrid by mutableStateOf(null) + internal val useGridLayout by mutableStateOf(getPref(AppPrefs.prefFeedGridLayout, false)) - private val swipeToRefresh: Boolean - get() = getPref(AppPrefs.prefSwipeToRefreshAll, true) + internal var selectMode by mutableStateOf(false) + } + + private val vm = SubscriptionsVM() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -170,47 +171,45 @@ class SubscriptionsFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { Logd(TAG, "fragment onCreateView") - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + vm.displayUpArrow = parentFragmentManager.backStackEntryCount != 0 + if (savedInstanceState != null) vm.displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) getSortingPrefs() - if (arguments != null) displayedFolder = requireArguments().getString(ARGUMENT_FOLDER, null) + if (arguments != null) vm.displayedFolder = requireArguments().getString(ARGUMENT_FOLDER, null) resetTags() val queues = realm.query(PlayQueue::class).find() - queueIds.addAll(queues.map { it.id }) - val spinnerTexts: MutableList = mutableListOf("Any queue", "No queue") - spinnerTexts.addAll(queues.map { it.name }) + vm.queueIds.addAll(queues.map { it.id }) + vm.spinnerTexts.addAll(queues.map { it.name }) - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - if (showFilterDialog) FilterDialog(FeedFilter(feedsFilter)) { showFilterDialog = false } - if (showSortDialog) SortDialog { showSortDialog = false } - if (showNewSynthetic) RenameOrCreateSyntheticFeed { showNewSynthetic = false } - Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { - InforBar() - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 20.dp, end = 20.dp)) { - Spinner(items = spinnerTexts, selectedIndex = queueFilterIndex) { index: Int -> - queueFilterIndex = index - loadSubscriptions() - } - Spacer(Modifier.weight(1f)) - Spinner(items = tags, selectedIndex = tagFilterIndex) { index: Int -> - tagFilterIndex = index - loadSubscriptions() - } - } - if (noSubscription) Text(stringResource(R.string.no_subscriptions_label)) - else LazyList() - } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { SubscriptionsScreen() } } } + vm.feedCount = vm.feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString() + loadSubscriptions() + return composeView + } + + @Composable + fun SubscriptionsScreen() { + if (vm.showFilterDialog) FilterDialog(FeedFilter(feedsFilter)) { vm.showFilterDialog = false } + if (vm.showSortDialog) SortDialog { vm.showSortDialog = false } + if (vm.showNewSynthetic) RenameOrCreateSyntheticFeed { vm.showNewSynthetic = false } + Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { + InforBar() + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 20.dp, end = 20.dp)) { + Spinner(items = vm.spinnerTexts, selectedIndex = queueFilterIndex) { index: Int -> + queueFilterIndex = index + loadSubscriptions() + } + Spacer(Modifier.weight(1f)) + Spinner(items = vm.tags, selectedIndex = tagFilterIndex) { index: Int -> + tagFilterIndex = index + loadSubscriptions() } } + if (vm.noSubscription) Text(stringResource(R.string.no_subscriptions_label)) + else LazyList() } } - feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString() - loadSubscriptions() - return composeView } override fun onStart() { @@ -227,12 +226,12 @@ class SubscriptionsFragment : Fragment() { override fun onDestroyView() { Logd(TAG, "onDestroyView") - feedListFiltered.clear() + vm.feedListFiltered.clear() super.onDestroyView() } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) + outState.putBoolean(KEY_UP_ARROW, vm.displayUpArrow) super.onSaveInstanceState(outState) } @@ -242,7 +241,7 @@ class SubscriptionsFragment : Fragment() { // TODO: #root appears not used in RealmDB, is it a SQLite specialty 1 -> " (tags.@count == 0 OR (tags.@count != 0 AND ALL tags == '#root' )) " else -> { // feeds with the chosen tag - val tag = tags[tagFilterIndex] + val tag = vm.tags[tagFilterIndex] " ANY tags == '$tag' " } } @@ -253,17 +252,17 @@ class SubscriptionsFragment : Fragment() { 0 -> "" // All feeds 1 -> " queueId == -2 " else -> { // feeds associated with the chosen queue - val qid = queueIds[queueFilterIndex-2] + val qid = vm.queueIds[queueFilterIndex-2] " queueId == '$qid' " } } } private fun resetTags() { - tags.clear() - tags.add("All tags") - tags.add("Untagged") - tags.addAll(getTags()) + vm.tags.clear() + vm.tags.add("All tags") + vm.tags.add("Untagged") + vm.tags.addAll(getTags()) } private var eventSink: Job? = null @@ -293,8 +292,8 @@ class SubscriptionsFragment : Fragment() { when (event) { is FlowEvent.FeedUpdatingEvent -> { Logd(TAG, "FeedUpdateRunningEvent: ${event.isRunning}") - infoTextUpdate = if (event.isRunning) " " + getString(R.string.refreshing_label) else "" - txtvInformation = (infoTextFiltered + infoTextUpdate) + vm.infoTextUpdate = if (event.isRunning) " " + getString(R.string.refreshing_label) else "" + vm.txtvInformation = (vm.infoTextFiltered + vm.infoTextUpdate) if (!event.isRunning && event.id != prevFeedUpdatingEvent?.id) loadSubscriptions() prevFeedUpdatingEvent = event } @@ -308,8 +307,8 @@ class SubscriptionsFragment : Fragment() { @Composable fun MyTopAppBar() { var expanded by remember { mutableStateOf(false) } - TopAppBar(title = { Text( if (displayedFolder.isNotEmpty()) displayedFolder else "") }, - navigationIcon = if (displayUpArrow) { + TopAppBar(title = { Text( if (vm.displayedFolder.isNotEmpty()) vm.displayedFolder else "") }, + navigationIcon = if (vm.displayUpArrow) { { IconButton(onClick = { parentFragmentManager.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } else { { IconButton(onClick = { (activity as? MainActivity)?.openDrawer() }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_subscriptions), contentDescription = "Open Drawer") } } @@ -317,16 +316,16 @@ class SubscriptionsFragment : Fragment() { actions = { IconButton(onClick = { (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_search), contentDescription = "search") } - IconButton(onClick = { showFilterDialog = true + IconButton(onClick = { vm.showFilterDialog = true }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_filter), contentDescription = "filter") } - IconButton(onClick = { showSortDialog = true + IconButton(onClick = { vm.showSortDialog = true }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.arrows_sort), contentDescription = "sort") } // IconButton(onClick = { // }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chart_box), contentDescription = "statistics") } IconButton(onClick = { expanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Menu") } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { DropdownMenuItem(text = { Text(stringResource(R.string.new_synth_label)) }, onClick = { - showNewSynthetic = true + vm.showNewSynthetic = true expanded = false }) DropdownMenuItem(text = { Text(stringResource(R.string.refresh_label)) }, onClick = { @@ -334,7 +333,7 @@ class SubscriptionsFragment : Fragment() { expanded = false }) DropdownMenuItem(text = { Text(stringResource(R.string.toggle_grid_list)) }, onClick = { - useGrid = if (useGrid == null) !useGridLayout else !useGrid!! + vm.useGrid = if (vm.useGrid == null) !vm.useGridLayout else !vm.useGrid!! expanded = false }) } @@ -346,7 +345,7 @@ class SubscriptionsFragment : Fragment() { private fun loadSubscriptions() { if (loadingJob != null) { loadingJob?.cancel() - feedListFiltered.clear() + vm.feedListFiltered.clear() } loadingJob = lifecycleScope.launch { val feedList: List @@ -356,13 +355,13 @@ class SubscriptionsFragment : Fragment() { feedList = fetchAndSort(false) } withContext(Dispatchers.Main) { - noSubscription = feedList.isEmpty() - feedListFiltered.clear() - feedListFiltered.addAll(feedList) - feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString() - infoTextFiltered = " " - if (feedsFilter.isNotEmpty()) infoTextFiltered = getString(R.string.filtered_label) - txtvInformation = (infoTextFiltered + infoTextUpdate) + vm.noSubscription = feedList.isEmpty() + vm.feedListFiltered.clear() + vm.feedListFiltered.addAll(feedList) + vm.feedCount = vm.feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString() + vm.infoTextFiltered = " " + if (feedsFilter.isNotEmpty()) vm.infoTextFiltered = getString(R.string.filtered_label) + vm.txtvInformation = (vm.infoTextFiltered + vm.infoTextUpdate) } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } }.apply { invokeOnCompletion { loadingJob = null } } @@ -387,11 +386,11 @@ class SubscriptionsFragment : Fragment() { val textColor = MaterialTheme.colorScheme.onSurface Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "info", tint = textColor) Spacer(Modifier.weight(1f)) - Text(txtvInformation, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { - if (feedsFilter.isNotEmpty()) showFilterDialog = true + Text(vm.txtvInformation, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { + if (feedsFilter.isNotEmpty()) vm.showFilterDialog = true } ) Spacer(Modifier.weight(1f)) - Text(feedCount, color = textColor) + Text(vm.feedCount, color = textColor) } } @@ -548,7 +547,7 @@ class SubscriptionsFragment : Fragment() { { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { showRemoveFeedDialog = true isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_delete: ${selected.size}") }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "") @@ -556,14 +555,14 @@ class SubscriptionsFragment : Fragment() { { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { showKeepUpdateDialog = true isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_refresh: ${selected.size}") }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_refresh), "") Text(stringResource(id = R.string.keep_updated)) } }, { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_download: ${selected.size}") showAutoDownloadSwitchDialog = true }) { @@ -572,14 +571,14 @@ class SubscriptionsFragment : Fragment() { { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { showAutoDeleteHandlerDialog = true isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_delete_auto: ${selected.size}") }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete_auto), "") Text(stringResource(id = R.string.auto_delete_label)) } }, { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_playback_speed: ${selected.size}") showSpeedDialog = true }) { @@ -587,7 +586,7 @@ class SubscriptionsFragment : Fragment() { Text(stringResource(id = R.string.playback_speed)) } }, { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_tag: ${selected.size}") showTagsSettingDialog = true }) { @@ -596,14 +595,14 @@ class SubscriptionsFragment : Fragment() { { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { showAssociateDialog = true isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "ic_playlist_play: ${selected.size}") // associatedQueuePrefHandler() }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "") Text(stringResource(id = R.string.pref_feed_associated_queue)) } }, { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { - selectMode = false + vm.selectMode = false Logd(TAG, "ic_star: ${selected.size}") showChooseRatingDialog = true isExpanded = false @@ -612,7 +611,7 @@ class SubscriptionsFragment : Fragment() { Text(stringResource(id = R.string.set_rating_label)) } }, { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { isExpanded = false - selectMode = false + vm.selectMode = false Logd(TAG, "baseline_import_export_24: ${selected.size}") val exportType = ExportTypes.OPML_SELECTED val title = String.format(exportType.outputNameTemplate, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date())) @@ -648,21 +647,21 @@ class SubscriptionsFragment : Fragment() { PullToRefreshBox(modifier = Modifier.fillMaxWidth(), isRefreshing = refreshing, indicator = {}, onRefresh = { // coroutineScope.launch { refreshing = true - if (swipeToRefresh) FeedUpdateManager.runOnceOrAsk(requireContext()) + if (getPref(AppPrefs.prefSwipeToRefreshAll, true)) FeedUpdateManager.runOnceOrAsk(requireContext()) refreshing = false // } }) { val context = LocalContext.current - if (if (useGrid == null) useGridLayout else useGrid!!) { + if (if (vm.useGrid == null) vm.useGridLayout else vm.useGrid!!) { val lazyGridState = rememberLazyGridState() LazyVerticalGrid(state = lazyGridState, columns = GridCells.Adaptive(80.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)) { - items(feedListFiltered.size, key = {index -> feedListFiltered[index].id}) { index -> - val feed by remember { mutableStateOf(feedListFiltered[index]) } + items(vm.feedListFiltered.size, key = {index -> vm.feedListFiltered[index].id}) { index -> + val feed by remember { mutableStateOf(vm.feedListFiltered[index]) } var isSelected by remember { mutableStateOf(false) } - LaunchedEffect(key1 = selectMode, key2 = selectedSize) { - isSelected = selectMode && feed in selected + LaunchedEffect(key1 = vm.selectMode, key2 = selectedSize) { + isSelected = vm.selectMode && feed in selected } fun toggleSelected() { isSelected = !isSelected @@ -673,15 +672,15 @@ class SubscriptionsFragment : Fragment() { .combinedClickable(onClick = { Logd(TAG, "clicked: ${feed.title}") if (!feed.isBuilding) { - if (selectMode) toggleSelected() + if (vm.selectMode) toggleSelected() else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) } }, onLongClick = { if (!feed.isBuilding) { - selectMode = !selectMode - isSelected = selectMode + vm.selectMode = !vm.selectMode + isSelected = vm.selectMode selected.clear() - if (selectMode) { + if (vm.selectMode) { selected.add(feed) longPressIndex = index } else { @@ -728,10 +727,10 @@ class SubscriptionsFragment : Fragment() { } else { val lazyListState = rememberLazyListState() LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(feedListFiltered, key = { _, feed -> feed.id}) { index, feed -> + itemsIndexed(vm.feedListFiltered, key = { _, feed -> feed.id}) { index, feed -> var isSelected by remember { mutableStateOf(false) } - LaunchedEffect(key1 = selectMode, key2 = selectedSize) { - isSelected = selectMode && feed in selected + LaunchedEffect(key1 = vm.selectMode, key2 = selectedSize) { + isSelected = vm.selectMode && feed in selected } fun toggleSelected() { isSelected = !isSelected @@ -749,7 +748,7 @@ class SubscriptionsFragment : Fragment() { modifier = Modifier.width(80.dp).height(80.dp).clickable(onClick = { Logd(TAG, "icon clicked!") if (!feed.isBuilding) { - if (selectMode) toggleSelected() + if (vm.selectMode) toggleSelected() else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) } }) @@ -758,15 +757,15 @@ class SubscriptionsFragment : Fragment() { Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = { Logd(TAG, "clicked: ${feed.title}") if (!feed.isBuilding) { - if (selectMode) toggleSelected() + if (vm.selectMode) toggleSelected() else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) } }, onLongClick = { if (!feed.isBuilding) { - selectMode = !selectMode - isSelected = selectMode + vm.selectMode = !vm.selectMode + isSelected = vm.selectMode selected.clear() - if (selectMode) { + if (vm.selectMode) { selected.add(feed) longPressIndex = index } else { @@ -790,7 +789,7 @@ class SubscriptionsFragment : Fragment() { Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.weight(1f)) var feedSortInfo by remember { mutableStateOf(feed.sortInfo) } - LaunchedEffect(feedSorted) { feedSortInfo = feed.sortInfo } + LaunchedEffect(vm.feedSorted) { feedSortInfo = feed.sortInfo } Text(feedSortInfo, color = textColor, style = MaterialTheme.typography.bodyMedium) } } @@ -800,30 +799,30 @@ class SubscriptionsFragment : Fragment() { } } } - if (selectMode) { + if (vm.selectMode) { val buttonColor = MaterialTheme.colorScheme.tertiary Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp).background(MaterialTheme.colorScheme.tertiaryContainer), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_upward_24), tint = buttonColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp).clickable(onClick = { selected.clear() - for (i in 0..longPressIndex) selected.add(feedListFiltered[i]) + for (i in 0..longPressIndex) selected.add(vm.feedListFiltered[i]) selectedSize = selected.size Logd(TAG, "selectedIds: ${selected.size}") })) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_downward_24), tint = buttonColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp).clickable(onClick = { selected.clear() - for (i in longPressIndex.. { - val dir = if (titleAscending) 1 else -1 + val dir = if (vm.titleAscending) 1 else -1 Comparator { lhs: Feed, rhs: Feed -> val t1 = lhs.title val t2 = rhs.title @@ -924,12 +923,12 @@ class SubscriptionsFragment : Fragment() { } } 1 -> { - val dir = if (dateAscending) 1 else -1 - when (dateSortIndex) { + val dir = if (vm.dateAscending) 1 else -1 + when (vm.dateSortIndex) { 0 -> { // date publish var playStateQueries = "" - for (i in playStateSort.indices) { - if (playStateSort[i].value) { + for (i in vm.playStateSort.indices) { + if (vm.playStateSort[i].value) { if (playStateQueries.isNotEmpty()) playStateQueries += " OR " playStateQueries += " playState == ${PlayState.entries[i].code} " } @@ -961,23 +960,23 @@ class SubscriptionsFragment : Fragment() { } } else -> { // count - val dir = if (countAscending) 1 else -1 + val dir = if (vm.countAscending) 1 else -1 var playStateQueries = "" - for (i in playStateSort.indices) { - if (playStateSort[i].value) { + for (i in vm.playStateSort.indices) { + if (vm.playStateSort[i].value) { if (playStateQueries.isNotEmpty()) playStateQueries += " OR " playStateQueries += " playState == ${PlayState.entries[i].code} " } } var ratingQueries = "" - for (i in ratingSort.indices) { - if (ratingSort[i].value) { + for (i in vm.ratingSort.indices) { + if (vm.ratingSort[i].value) { if (ratingQueries.isNotEmpty()) ratingQueries += " OR " ratingQueries += " rating == ${Rating.entries[i].code} " } } - val downloadedQuery = if (downlaodedSortIndex == 0) " downloaded == true " else if (downlaodedSortIndex == 1) " downloaded == false " else "" - val commentedQuery = if (commentedSortIndex == 0) " comment != '' " else if (commentedSortIndex == 1) " comment == '' " else "" + val downloadedQuery = if (vm.downlaodedSortIndex == 0) " downloaded == true " else if (vm.downlaodedSortIndex == 1) " downloaded == false " else "" + val commentedQuery = if (vm.commentedSortIndex == 0) " comment != '' " else if (vm.commentedSortIndex == 1) " comment == '' " else "" var queryString = "feedId == $0" if (playStateQueries.isNotEmpty()) queryString += " AND ($playStateQueries)" @@ -994,12 +993,12 @@ class SubscriptionsFragment : Fragment() { comparator(counterMap, dir) } } - feedSorted++ + vm.feedSorted++ if (!build) return feedList_.sortedWith(comparator) saveSortingPrefs() - feedListFiltered.clear() - feedListFiltered.addAll(feedList_.sortedWith(comparator)) + vm.feedListFiltered.clear() + vm.feedListFiltered.addAll(feedList_.sortedWith(comparator)) return listOf() } @@ -1022,43 +1021,43 @@ class SubscriptionsFragment : Fragment() { val scrollState = rememberScrollState() Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { Row { - OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 0) textColor else Color.Green), + OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (vm.sortIndex != 0) textColor else Color.Green), onClick = { - titleAscending = !titleAscending - sortIndex = 0 + vm.titleAscending = !vm.titleAscending + vm.sortIndex = 0 fetchAndSortRoutine() } - ) { Text(text = stringResource(R.string.title) + if (titleAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } + ) { Text(text = stringResource(R.string.title) + if (vm.titleAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } Spacer(Modifier.weight(1f)) - OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 1) textColor else Color.Green), + OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (vm.sortIndex != 1) textColor else Color.Green), onClick = { - dateAscending = !dateAscending - sortIndex = 1 + vm.dateAscending = !vm.dateAscending + vm.sortIndex = 1 fetchAndSortRoutine() } - ) { Text(text = stringResource(R.string.date) + if (dateAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } + ) { Text(text = stringResource(R.string.date) + if (vm.dateAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } Spacer(Modifier.weight(1f)) - OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 2) textColor else Color.Green), + OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (vm.sortIndex != 2) textColor else Color.Green), onClick = { - countAscending = !countAscending - sortIndex = 2 + vm.countAscending = !vm.countAscending + vm.sortIndex = 2 fetchAndSortRoutine() } - ) { Text(text = stringResource(R.string.count) + if (countAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } + ) { Text(text = stringResource(R.string.count) + if (vm.countAscending) "\u00A0▲" else "\u00A0▼", color = textColor) } } HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp) - if (sortIndex == 1) { + if (vm.sortIndex == 1) { Row { - OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (dateSortIndex != 0) textColor else Color.Green), + OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (vm.dateSortIndex != 0) textColor else Color.Green), onClick = { - dateSortIndex = 0 + vm.dateSortIndex = 0 fetchAndSortRoutine() } ) { Text(stringResource(R.string.publish_date)) } Spacer(Modifier.weight(1f)) - OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (dateSortIndex != 1) textColor else Color.Green), + OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (vm.dateSortIndex != 1) textColor else Color.Green), onClick = { - dateSortIndex = 1 + vm.dateSortIndex = 1 fetchAndSortRoutine() } ) { Text(stringResource(R.string.download_date)) } @@ -1066,31 +1065,31 @@ class SubscriptionsFragment : Fragment() { } HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp) Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) { - if (sortIndex == 2) { + if (vm.sortIndex == 2) { Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { val item = EpisodeFilter.EpisodesFilterGroup.DOWNLOADED var selectNone by remember { mutableStateOf(false) } - if (selectNone) downlaodedSortIndex = -1 + if (selectNone) vm.downlaodedSortIndex = -1 Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.padding(end = 10.dp)) Spacer(Modifier.weight(0.3f)) OutlinedButton( - modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (downlaodedSortIndex != 0) textColor else Color.Green), + modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (vm.downlaodedSortIndex != 0) textColor else Color.Green), onClick = { - if (downlaodedSortIndex != 0) { + if (vm.downlaodedSortIndex != 0) { selectNone = false - downlaodedSortIndex = 0 - } else downlaodedSortIndex = -1 + vm.downlaodedSortIndex = 0 + } else vm.downlaodedSortIndex = -1 fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[0].displayName), color = textColor) } Spacer(Modifier.weight(0.1f)) OutlinedButton( - modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (downlaodedSortIndex != 1) textColor else Color.Green), + modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (vm.downlaodedSortIndex != 1) textColor else Color.Green), onClick = { - if (downlaodedSortIndex != 1) { + if (vm.downlaodedSortIndex != 1) { selectNone = false - downlaodedSortIndex = 1 - } else downlaodedSortIndex = -1 + vm.downlaodedSortIndex = 1 + } else vm.downlaodedSortIndex = -1 fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[1].displayName), color = textColor) } @@ -1099,34 +1098,34 @@ class SubscriptionsFragment : Fragment() { Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { val item = EpisodeFilter.EpisodesFilterGroup.OPINION var selectNone by remember { mutableStateOf(false) } - if (selectNone) commentedSortIndex = -1 + if (selectNone) vm.commentedSortIndex = -1 Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.padding(end = 10.dp)) Spacer(Modifier.weight(0.3f)) OutlinedButton( - modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (commentedSortIndex != 0) textColor else Color.Green), + modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (vm.commentedSortIndex != 0) textColor else Color.Green), onClick = { - if (commentedSortIndex != 0) { + if (vm.commentedSortIndex != 0) { selectNone = false - commentedSortIndex = 0 - } else commentedSortIndex = -1 + vm.commentedSortIndex = 0 + } else vm.commentedSortIndex = -1 fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[0].displayName), color = textColor) } Spacer(Modifier.weight(0.1f)) OutlinedButton( - modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (commentedSortIndex != 1) textColor else Color.Green), + modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (vm.commentedSortIndex != 1) textColor else Color.Green), onClick = { - if (commentedSortIndex != 1) { + if (vm.commentedSortIndex != 1) { selectNone = false - commentedSortIndex = 1 - } else commentedSortIndex = -1 + vm.commentedSortIndex = 1 + } else vm.commentedSortIndex = -1 fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[1].displayName), color = textColor) } Spacer(Modifier.weight(0.5f)) } } - if ((sortIndex == 1 && dateSortIndex == 0) || sortIndex == 2) { + if ((vm.sortIndex == 1 && vm.dateSortIndex == 0) || vm.sortIndex == 2) { val item = EpisodeFilter.EpisodesFilterGroup.PLAY_STATE var selectNone by remember { mutableStateOf(false) } var expandRow by remember { mutableStateOf(false) } @@ -1138,13 +1137,13 @@ class SubscriptionsFragment : Fragment() { Spacer(Modifier.weight(1f)) if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable { - val hIndex = playStateSort.indexOfLast { it.value } + val hIndex = vm.playStateSort.indexOfLast { it.value } if (hIndex < 0) return@clickable if (!lowerSelected) { - for (i in 0..hIndex) playStateSort[i].value = true + for (i in 0..hIndex) vm.playStateSort[i].value = true } else { - for (i in 0..hIndex) playStateSort[i].value = false - playStateSort[hIndex].value = true + for (i in 0..hIndex) vm.playStateSort[i].value = false + vm.playStateSort[hIndex].value = true } lowerSelected = !lowerSelected fetchAndSortRoutine() @@ -1154,19 +1153,19 @@ class SubscriptionsFragment : Fragment() { modifier = Modifier.clickable { lowerSelected = false higherSelected = false - for (i in item.values.indices) playStateSort[i].value = false + for (i in item.values.indices) vm.playStateSort[i].value = false fetchAndSortRoutine() }) Spacer(Modifier.weight(1f)) if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable { - val lIndex = playStateSort.indexOfFirst { it.value } + val lIndex = vm.playStateSort.indexOfFirst { it.value } if (lIndex < 0) return@clickable if (!higherSelected) { - for (i in lIndex.. - if (selectNone) playStateSort[index].value = false + if (selectNone) vm.playStateSort[index].value = false LaunchedEffect(Unit) { // if (filter != null && item.values[index].filterId in filter.properties) selectedList[index].value = true } OutlinedButton( modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(), - border = BorderStroke(2.dp, if (playStateSort[index].value) Color.Green else textColor), + border = BorderStroke(2.dp, if (vm.playStateSort[index].value) Color.Green else textColor), onClick = { selectNone = false - playStateSort[index].value = !playStateSort[index].value + vm.playStateSort[index].value = !vm.playStateSort[index].value fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor) } } } - if (sortIndex == 2) { + if (vm.sortIndex == 2) { val item = EpisodeFilter.EpisodesFilterGroup.RATING var selectNone by remember { mutableStateOf(false) } var expandRow by remember { mutableStateOf(false) } @@ -1201,13 +1200,13 @@ class SubscriptionsFragment : Fragment() { Spacer(Modifier.weight(1f)) if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable { - val hIndex = ratingSort.indexOfLast { it.value } + val hIndex = vm.ratingSort.indexOfLast { it.value } if (hIndex < 0) return@clickable if (!lowerSelected) { - for (i in 0..hIndex) ratingSort[i].value = true + for (i in 0..hIndex) vm.ratingSort[i].value = true } else { - for (i in 0..hIndex) ratingSort[i].value = false - ratingSort[hIndex].value = true + for (i in 0..hIndex) vm.ratingSort[i].value = false + vm.ratingSort[hIndex].value = true } lowerSelected = !lowerSelected fetchAndSortRoutine() @@ -1217,19 +1216,19 @@ class SubscriptionsFragment : Fragment() { modifier = Modifier.clickable { lowerSelected = false higherSelected = false - for (i in item.values.indices) ratingSort[i].value = false + for (i in item.values.indices) vm.ratingSort[i].value = false fetchAndSortRoutine() }) Spacer(Modifier.weight(1f)) if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable { - val lIndex = ratingSort.indexOfFirst { it.value } + val lIndex = vm.ratingSort.indexOfFirst { it.value } if (lIndex < 0) return@clickable if (!higherSelected) { - for (i in lIndex.. - if (selectNone) ratingSort[index].value = false + if (selectNone) vm.ratingSort[index].value = false OutlinedButton( modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(), - border = BorderStroke(2.dp, if (ratingSort[index].value) Color.Green else textColor), + border = BorderStroke(2.dp, if (vm.ratingSort[index].value) Color.Green else textColor), onClick = { selectNone = false - ratingSort[index].value = !ratingSort[index].value + vm.ratingSort[index].value = !vm.ratingSort[index].value fetchAndSortRoutine() }, ) { Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor) } diff --git a/app/src/main/res/layout/alternate_urls_dropdown_item.xml b/app/src/main/res/layout/alternate_urls_dropdown_item.xml deleted file mode 100644 index 82de8a02..00000000 --- a/app/src/main/res/layout/alternate_urls_dropdown_item.xml +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/app/src/main/res/layout/alternate_urls_item.xml b/app/src/main/res/layout/alternate_urls_item.xml deleted file mode 100644 index 9fdb50e3..00000000 --- a/app/src/main/res/layout/alternate_urls_item.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/app/src/main/res/layout/select_country_dialog.xml b/app/src/main/res/layout/select_country_dialog.xml deleted file mode 100644 index cea1eb77..00000000 --- a/app/src/main/res/layout/select_country_dialog.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/wifi_sync_dialog.xml b/app/src/main/res/layout/wifi_sync_dialog.xml deleted file mode 100644 index 8027f022..00000000 --- a/app/src/main/res/layout/wifi_sync_dialog.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - -