6.13.8 commit
This commit is contained in:
parent
50dc8e1330
commit
08822bd8ac
|
@ -26,8 +26,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
versionCode 3020294
|
||||
versionName "6.13.7"
|
||||
versionCode 3020295
|
||||
versionName "6.13.8"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -1157,8 +1157,10 @@ class PlaybackService : MediaLibraryService() {
|
|||
media.setPosition(position)
|
||||
media.setLastPlayedTime(System.currentTimeMillis())
|
||||
if (it.isNew) it.playState = PlayState.UNPLAYED.code
|
||||
if (media.startPosition >= 0 && media.getPosition() > media.startPosition)
|
||||
if (media.startPosition >= 0 && media.getPosition() > media.startPosition) {
|
||||
media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition)
|
||||
media.timeSpent = (System.currentTimeMillis() - media.startTime).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
// This appears not too useful
|
||||
|
|
|
@ -188,7 +188,6 @@ class AboutFragment : PreferenceFragmentCompat() {
|
|||
else -> DevelopersFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return TOTAL_COUNT
|
||||
}
|
||||
|
|
|
@ -36,10 +36,10 @@ object LogsAndStats {
|
|||
* Searches the DB for statistics.
|
||||
* @return The list of statistics objects
|
||||
*/
|
||||
fun getStatistics(includeMarkedAsPlayed: Boolean, timeFilterFrom: Long, timeFilterTo: Long): StatisticsResult {
|
||||
fun getStatistics(includeMarkedAsPlayed: Boolean, timeFilterFrom: Long, timeFilterTo: Long, feedId: Long = 0L): StatisticsResult {
|
||||
Logd(TAG, "getStatistics called")
|
||||
val medias = if (feedId == 0L) realm.query(EpisodeMedia::class).find() else realm.query(EpisodeMedia::class).query("episode.feedId == $feedId").find()
|
||||
|
||||
val medias = realm.query(EpisodeMedia::class).find()
|
||||
val groupdMedias = medias.groupBy { it.episodeOrFetch()?.feedId ?: 0L }
|
||||
val result = StatisticsResult()
|
||||
result.oldestDate = Long.MAX_VALUE
|
||||
|
@ -47,6 +47,8 @@ object LogsAndStats {
|
|||
val feed = getFeed(fid, false) ?: continue
|
||||
val numEpisodes = feed.episodes.size.toLong()
|
||||
var feedPlayedTime = 0L
|
||||
var timeSpent = 0L
|
||||
var durationWithSkip = 0L
|
||||
var feedTotalTime = 0L
|
||||
var episodesStarted = 0L
|
||||
var totalDownloadSize = 0L
|
||||
|
@ -54,16 +56,20 @@ object LogsAndStats {
|
|||
for (m in feedMedias) {
|
||||
if (m.lastPlayedTime > 0 && m.lastPlayedTime < result.oldestDate) result.oldestDate = m.lastPlayedTime
|
||||
feedTotalTime += m.duration
|
||||
if (m.lastPlayedTime in timeFilterFrom..<timeFilterTo) {
|
||||
if (m.lastPlayedTime in (timeFilterFrom + 1)..<timeFilterTo) {
|
||||
if (includeMarkedAsPlayed) {
|
||||
if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || (m.episodeOrFetch()?.playState?:-10) > PlayState.SKIPPED.code || m.position > 0) {
|
||||
episodesStarted += 1
|
||||
feedPlayedTime += m.duration
|
||||
timeSpent += m.timeSpent
|
||||
}
|
||||
} else {
|
||||
feedPlayedTime += m.playedDuration
|
||||
timeSpent += m.timeSpent
|
||||
Logd(TAG, "m.playedDuration: ${m.playedDuration} m.timeSpent: ${m.timeSpent}")
|
||||
if (m.playbackCompletionTime > 0 && m.playedDuration > 0) episodesStarted += 1
|
||||
}
|
||||
durationWithSkip += m.duration
|
||||
}
|
||||
if (m.downloaded) {
|
||||
episodesDownloadCount += 1
|
||||
|
@ -71,8 +77,10 @@ object LogsAndStats {
|
|||
}
|
||||
}
|
||||
feedPlayedTime /= 1000
|
||||
durationWithSkip /= 1000
|
||||
timeSpent /= 1000
|
||||
feedTotalTime /= 1000
|
||||
result.statsItems.add(StatisticsItem(feed, feedTotalTime, feedPlayedTime, numEpisodes, episodesStarted, totalDownloadSize, episodesDownloadCount))
|
||||
result.statsItems.add(StatisticsItem(feed, feedTotalTime, feedPlayedTime, timeSpent, durationWithSkip, numEpisodes, episodesStarted, totalDownloadSize, episodesDownloadCount))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ object RealmDB {
|
|||
SubscriptionLog::class,
|
||||
Chapter::class))
|
||||
.name("Podcini.realm")
|
||||
.schemaVersion(29)
|
||||
.schemaVersion(30)
|
||||
.migration({ mContext ->
|
||||
val oldRealm = mContext.oldRealm // old realm using the previous schema
|
||||
val newRealm = mContext.newRealm // new realm using the new schema
|
||||
|
@ -105,7 +105,7 @@ object RealmDB {
|
|||
// }
|
||||
}
|
||||
if (oldRealm.schemaVersion() < 28) {
|
||||
Logd(TAG, "migrating DB from below 27")
|
||||
Logd(TAG, "migrating DB from below 28")
|
||||
mContext.enumerate(className = "Episode") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? ->
|
||||
newObject?.run {
|
||||
if (oldObject.getValue<Long>(fieldName = "playState") == 1L) {
|
||||
|
@ -113,12 +113,29 @@ object RealmDB {
|
|||
} else {
|
||||
val media = oldObject.getObject(propertyName = "media")
|
||||
var position = 0L
|
||||
if (media != null) position = media.getValue(propertyName = "position", Long::class) ?: 0
|
||||
if (media != null) position = media.getValue(propertyName = "position", Long::class)
|
||||
if (position > 0) set("playState", 5L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldRealm.schemaVersion() < 30) {
|
||||
Logd(TAG, "migrating DB from below 30")
|
||||
mContext.enumerate(className = "Episode") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? ->
|
||||
newObject?.run {
|
||||
val media = oldObject.getObject(propertyName = "media")
|
||||
var playedDuration = 0L
|
||||
if (media != null) {
|
||||
playedDuration = media.getValue(propertyName = "playedDuration", Long::class)
|
||||
Logd(TAG, "position: $playedDuration")
|
||||
if (playedDuration > 0L) {
|
||||
val newMedia = newObject.getObject(propertyName = "media")
|
||||
newMedia?.set("timeSpent", playedDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).build()
|
||||
realm = Realm.open(config)
|
||||
}
|
||||
|
|
|
@ -17,9 +17,6 @@ import org.apache.commons.lang3.builder.ToStringBuilder
|
|||
import org.apache.commons.lang3.builder.ToStringStyle
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Episode within a feed.
|
||||
*/
|
||||
class Episode : RealmObject {
|
||||
@PrimaryKey
|
||||
var id: Long = 0L // increments from Date().time * 100 at time of creation
|
||||
|
|
|
@ -45,8 +45,17 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
@set:JvmName("setLastPlayedTimeProperty")
|
||||
var lastPlayedTime: Long = 0 // Last time this media was played (in ms)
|
||||
|
||||
var startPosition: Int = -1
|
||||
|
||||
var playedDurationWhenStarted: Int = 0
|
||||
private set
|
||||
|
||||
var playedDuration: Int = 0 // How many ms of this file have been played
|
||||
|
||||
var startTime: Long = 0 // time in ms when start playing
|
||||
|
||||
var timeSpent: Int = 0 // How many ms of this file have been played in actual time
|
||||
|
||||
// File size in Byte
|
||||
var size: Long = 0L
|
||||
|
||||
|
@ -64,11 +73,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
}
|
||||
var playbackCompletionTime: Long = 0
|
||||
|
||||
var startPosition: Int = -1
|
||||
|
||||
var playedDurationWhenStarted: Int = 0
|
||||
private set
|
||||
|
||||
@Ignore
|
||||
var volumeAdaptionSetting: VolumeAdaptionSetting = VolumeAdaptionSetting.OFF
|
||||
get() = fromInteger(volumeAdaption)
|
||||
|
@ -286,6 +290,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
override fun onPlaybackStart() {
|
||||
startPosition = max(position.toDouble(), 0.0).toInt()
|
||||
playedDurationWhenStarted = playedDuration
|
||||
startTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun onPlaybackPause(context: Context) {
|
||||
|
@ -294,6 +299,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
playedDuration = playedDurationWhenStarted + position - startPosition
|
||||
playedDurationWhenStarted = playedDuration
|
||||
}
|
||||
timeSpent = (System.currentTimeMillis() - startTime).toInt()
|
||||
startPosition = position
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
|
|||
query.append(r)
|
||||
}
|
||||
query.append(") ")
|
||||
Logd("FeedFilter", "audoDeleteQueues: ${query}")
|
||||
Logd("FeedFilter", "audoDeleteQueues: $query")
|
||||
statements.add(query.toString())
|
||||
}
|
||||
when {
|
||||
|
@ -90,7 +90,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
|
|||
query.append(r)
|
||||
}
|
||||
query.append(") ")
|
||||
Logd("queryString", "${query}")
|
||||
Logd("queryString", "$query")
|
||||
return query.toString()
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.util.ArrayList
|
|||
class StatisticsItem(val feed: Feed,
|
||||
val time: Long, // total time, in seconds
|
||||
val timePlayed: Long, // in seconds, Respects speed, listening twice, ...
|
||||
val timeSpent: Long, // in seconds, actual time spent playing
|
||||
val durationOfStarted: Long, // in seconds, total duration of episodes started playing
|
||||
val numEpisodes: Long, // Number of episodes.
|
||||
val episodesStarted: Long, // Episodes that are actually played.
|
||||
val totalDownloadSize: Long, // Simply sums up the size of download podcasts.
|
||||
|
@ -15,6 +17,7 @@ class MonthlyStatisticsItem {
|
|||
var year: Int = 0
|
||||
var month: Int = 0
|
||||
var timePlayed: Long = 0
|
||||
var timeSpent: Long = 0
|
||||
}
|
||||
|
||||
class StatisticsResult {
|
||||
|
|
|
@ -101,8 +101,8 @@ object DurationConverter {
|
|||
* @return "HH:MM hours"
|
||||
*/
|
||||
@JvmStatic
|
||||
fun shortLocalizedDuration(context: Context, time: Long): String {
|
||||
fun shortLocalizedDuration(context: Context, time: Long, showHoursText: Boolean = true): String {
|
||||
val hours = time.toFloat() / 3600f
|
||||
return String.format(Locale.getDefault(), "%.2f ", hours) + context.getString(R.string.time_hours)
|
||||
return String.format(Locale.getDefault(), "%.2f ", hours) + if (showHoursText) context.getString(R.string.time_hours) else ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import java.util.*
|
|||
object EpisodesPermutors {
|
||||
/**
|
||||
* Returns a Permutor that sorts a list appropriate to the given sort order.
|
||||
*
|
||||
* @return Permutor that sorts a list appropriate to the given sort order.
|
||||
*/
|
||||
@JvmStatic
|
||||
|
@ -100,20 +99,16 @@ object EpisodesPermutors {
|
|||
}
|
||||
|
||||
/**
|
||||
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in
|
||||
* the queue.
|
||||
*
|
||||
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
|
||||
* A listener might want to hear episodes from any given feed in pubdate order, but would
|
||||
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
|
||||
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
|
||||
*
|
||||
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
|
||||
* This method first starts with a queue of the final size, where each slot is empty (null).
|
||||
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
|
||||
* The podcast with the second-most number of episodes (`D`) is then
|
||||
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
|
||||
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
|
||||
*
|
||||
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
|
||||
*
|
||||
* @param queue A (modifiable) list of FeedItem elements to be reordered.
|
||||
|
@ -172,8 +167,7 @@ object EpisodesPermutors {
|
|||
|
||||
/**
|
||||
* Interface for passing around list permutor method. This is used for cases where a simple comparator
|
||||
* won't work (e.g. Random, Smart Shuffle, etc).
|
||||
*
|
||||
* won't work (e.g. Random, Smart Shuffle, etc)
|
||||
* @param <E> the type of elements in the list
|
||||
</E> */
|
||||
interface Permutor<E> {
|
||||
|
|
|
@ -35,6 +35,7 @@ import android.view.KeyEvent
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -102,7 +104,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
|
|||
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val label = getLabel()
|
||||
Logd(TAG, "button label: $label")
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
|
@ -151,7 +152,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
(fragment.view as? ViewGroup)?.removeView(this@apply)
|
||||
}) {
|
||||
val context = LocalContext.current
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (action in swipeActions) {
|
||||
if (action.getId() == NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue
|
||||
|
@ -614,7 +615,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
var showPickerDialog by remember { mutableStateOf(false) }
|
||||
if (showPickerDialog) {
|
||||
Dialog(onDismissRequest = { showPickerDialog = false }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.padding(16.dp)) {
|
||||
items(keys.size) { index ->
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp).clickable {
|
||||
|
@ -663,7 +664,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
else -> {}
|
||||
}
|
||||
if (tag != QueuesFragment.TAG) keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_QUEUE.name) }
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Text(stringResource(R.string.swipeactions_label) + " - " + forFragment)
|
||||
Text(stringResource(R.string.swipe_left))
|
||||
|
|
|
@ -28,7 +28,7 @@ import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
|||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.fragment.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr
|
||||
import ac.mdiq.podcini.ui.utils.TransitionEffect
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
|
|
|
@ -8,6 +8,7 @@ import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
|||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLocalized
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -20,6 +21,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
@ -35,7 +37,7 @@ fun ChaptersDialog(media: Playable, onDismissRequest: () -> Unit) {
|
|||
val chapters = media.getChapters()
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(stringResource(R.string.chapters_label))
|
||||
var currentChapterIndex by remember { mutableIntStateOf(-1) }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -135,7 +137,7 @@ fun AutoCompleteTextView(suggestions: List<String>, onItemSelected: (String) ->
|
|||
@Composable
|
||||
fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Add comment", color = textColor, style = MaterialTheme.typography.titleLarge)
|
||||
|
@ -183,4 +185,4 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
//import ac.mdiq.podcini.ui.actions.EpisodeActionButton.Companion.forItem
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
|
@ -233,7 +232,7 @@ class EpisodeVM(var episode: Episode) {
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||
|
@ -255,7 +254,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (state in PlayState.entries) {
|
||||
if (state.userSet) {
|
||||
|
@ -313,7 +312,7 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
var removeChecked by remember { mutableStateOf(false) }
|
||||
|
@ -365,7 +364,7 @@ fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find()
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
|
@ -429,7 +428,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
|
|||
val context = LocalContext.current
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
|
||||
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message + ": ${selected.size}")
|
||||
|
@ -955,7 +954,7 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
|||
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
var audioOnly by remember { mutableStateOf(false) }
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
|
|
|
@ -50,7 +50,7 @@ import java.util.*
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||
|
@ -77,7 +77,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
|||
val context = LocalContext.current
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message)
|
||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||
|
@ -131,7 +131,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
|||
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp))
|
||||
|
|
|
@ -63,6 +63,7 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -340,7 +341,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
VolumeAdaptionSetting.entries.forEach { item ->
|
||||
|
|
|
@ -21,10 +21,7 @@ import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
|
|||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.LargeTextEditingDialog
|
||||
import ac.mdiq.podcini.ui.compose.RemoveFeedDialog
|
||||
import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment
|
||||
import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment.Companion.EXTRA_DETAILED
|
||||
import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment.Companion.EXTRA_FEED_ID
|
||||
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.fragment.StatisticsFragment.Companion.FeedStatisticsDialog
|
||||
import ac.mdiq.podcini.ui.utils.TransitionEffect
|
||||
import ac.mdiq.podcini.util.*
|
||||
import android.R.string
|
||||
|
@ -68,9 +65,7 @@ import androidx.constraintlayout.compose.ConstraintLayout
|
|||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -194,16 +189,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
Spacer(modifier = Modifier.width(15.dp))
|
||||
}
|
||||
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner",
|
||||
// Modifier.width(12.dp).height(12.dp).constrainAs(image1) {
|
||||
// bottom.linkTo(parent.bottom)
|
||||
// start.linkTo(parent.start)
|
||||
// })
|
||||
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner",
|
||||
// Modifier.width(12.dp).height(12.dp).constrainAs(image2) {
|
||||
// bottom.linkTo(parent.bottom)
|
||||
// end.linkTo(parent.end)
|
||||
// })
|
||||
Row(verticalAlignment = Alignment.Top, modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).constrainAs(imgvCover) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
|
@ -237,6 +222,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
rating = feed.rating
|
||||
}
|
||||
})
|
||||
var showFeedStats by remember { mutableStateOf(false) }
|
||||
if (showFeedStats) FeedStatisticsDialog(feed.title?: "No title", feed.id) { showFeedStats = false }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).verticalScroll(scrollState)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
|
@ -292,19 +279,13 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Button(modifier = Modifier.padding(top = 10.dp), onClick = {
|
||||
val fragment = SearchResultsFragment.newInstance(CombinedSearcher::class.java, "$txtvAuthor podcasts")
|
||||
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||
}) {
|
||||
Text(stringResource(R.string.feeds_related_to_author))
|
||||
}
|
||||
}) { Text(stringResource(R.string.feeds_related_to_author)) }
|
||||
}
|
||||
Text(stringResource(R.string.statistics_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp))
|
||||
val arguments = Bundle()
|
||||
arguments.putLong(EXTRA_FEED_ID, feed.id)
|
||||
arguments.putBoolean(EXTRA_DETAILED, false)
|
||||
AndroidFragment(clazz = FeedStatisticsFragment::class.java, arguments = arguments)
|
||||
Button({
|
||||
(activity as MainActivity).loadChildFragment(StatisticsFragment(), TransitionEffect.SLIDE)
|
||||
}) {
|
||||
Text(stringResource(R.string.statistics_view_all))
|
||||
Row {
|
||||
Button({ showFeedStats = true }) { Text(stringResource(R.string.statistics_view_this)) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button({ (activity as MainActivity).loadChildFragment(StatisticsFragment(), TransitionEffect.SLIDE) }) { Text(stringResource(R.string.statistics_view_all)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import android.widget.CompoundButton
|
|||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
@ -44,6 +45,7 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
|
@ -421,7 +423,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
videoModeTags.forEach { text ->
|
||||
|
@ -475,7 +477,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
FeedAutoDeleteOptions.forEach { text ->
|
||||
|
@ -512,7 +514,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
VolumeAdaptionSetting.entries.forEach { item ->
|
||||
|
@ -542,7 +544,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
AutoDownloadPolicy.entries.forEach { item ->
|
||||
|
@ -572,7 +574,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
fun SetEpisodesCacheDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
|
||||
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
|
||||
|
@ -594,7 +596,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
queueSettingOptions.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -647,7 +649,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -689,7 +691,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -730,7 +732,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val oldName = feed?.preferences?.username?:""
|
||||
var newName by remember { mutableStateOf(oldName) }
|
||||
|
@ -758,7 +760,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
fun AutoSkipDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
|
||||
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
|
||||
|
|
|
@ -31,6 +31,7 @@ import android.view.*
|
|||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -402,7 +403,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
else -> ""
|
||||
}
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
@ -430,7 +431,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun SubscriptionDetailDialog(log: SubscriptionLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
@ -468,7 +469,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
val messageFull = requireContext().getString(R.string.download_log_details_message, requireContext().getString(from(status.reason)), message, url)
|
||||
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), ) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
|
|
@ -12,7 +12,6 @@ import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
|||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ARGUMENT_FEED_ID
|
||||
import ac.mdiq.podcini.ui.fragment.HistoryFragment.Companion.getNumberOfPlayed
|
||||
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.R.attr
|
||||
|
|
|
@ -44,6 +44,7 @@ import android.util.Log
|
|||
import android.view.*
|
||||
import android.widget.CheckBox
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
|
@ -541,7 +542,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newName by remember { mutableStateOf(curQueue.name) }
|
||||
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") })
|
||||
|
@ -565,7 +566,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun AddQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newName by remember { mutableStateOf("") }
|
||||
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") })
|
||||
|
|
|
@ -0,0 +1,664 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.PagerFragmentBinding
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats.getStatistics
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.update
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
|
||||
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.text.format.DateFormat
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class StatisticsFragment : Fragment() {
|
||||
private lateinit var tabLayout: TabLayout
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private var _binding: PagerFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
_binding = PagerFragmentBinding.inflate(inflater)
|
||||
viewPager = binding.viewpager
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = getString(R.string.statistics_label)
|
||||
toolbar.inflateMenu(R.menu.statistics)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
(activity as MainActivity).setupToolbarToggle(toolbar, false)
|
||||
|
||||
viewPager.adapter = PagerAdapter(this)
|
||||
// Give the TabLayout the ViewPager
|
||||
tabLayout = binding.slidingTabs
|
||||
setupPagedToolbar(toolbar, viewPager)
|
||||
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab: TabLayout.Tab, position: Int ->
|
||||
when (position) {
|
||||
POS_SUBSCRIPTIONS -> tab.setText(R.string.subscriptions_label)
|
||||
POS_YEARS -> tab.setText(R.string.months_statistics_label)
|
||||
POS_SPACE_TAKEN -> tab.setText(R.string.downloads_label)
|
||||
else -> {}
|
||||
}
|
||||
}.attach()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.statistics_reset) {
|
||||
confirmResetStatistics()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setupPagedToolbar(toolbar: MaterialToolbar, viewPager: ViewPager2) {
|
||||
this.toolbar = toolbar
|
||||
this.viewPager = viewPager
|
||||
|
||||
toolbar.setOnMenuItemClickListener { item: MenuItem? ->
|
||||
if (this.onOptionsItemSelected(item!!)) return@setOnMenuItemClickListener true
|
||||
val child = childFragmentManager.findFragmentByTag("f" + viewPager.currentItem)
|
||||
if (child != null) return@setOnMenuItemClickListener child.onOptionsItemSelected(item)
|
||||
false
|
||||
}
|
||||
viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
val child = childFragmentManager.findFragmentByTag("f$position")
|
||||
child?.onPrepareOptionsMenu(toolbar.menu)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun confirmResetStatistics() {
|
||||
val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(),
|
||||
R.string.statistics_reset_data, R.string.statistics_reset_data_msg) {
|
||||
override fun onConfirmButtonPressed(dialog: DialogInterface) {
|
||||
dialog.dismiss()
|
||||
doResetStatistics()
|
||||
}
|
||||
}
|
||||
conDialog.createNewDialog().show()
|
||||
}
|
||||
|
||||
private fun doResetStatistics() {
|
||||
prefs!!.edit()
|
||||
.putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
.putLong(PREF_FILTER_FROM, 0)
|
||||
.putLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
.apply()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { resetStatistics() }
|
||||
EventFlow.postEvent(FlowEvent.StatisticsEvent())
|
||||
} catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetStatistics(): Job {
|
||||
return runOnIOScope {
|
||||
val mediaAll = realm.query(EpisodeMedia::class).find()
|
||||
for (m in mediaAll) update(m) { m.playedDuration = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
private class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
POS_SUBSCRIPTIONS -> SubscriptionStatisticsFragment()
|
||||
POS_YEARS -> MonthlyStatisticsFragment()
|
||||
POS_SPACE_TAKEN -> DownloadStatisticsFragment()
|
||||
else -> DownloadStatisticsFragment()
|
||||
}
|
||||
}
|
||||
override fun getItemCount(): Int {
|
||||
return TOTAL_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
class SubscriptionStatisticsFragment : Fragment() {
|
||||
lateinit var statisticsData: StatisticsResult
|
||||
private lateinit var lineChartData: LineChartData
|
||||
private var timeSpentSum = 0L
|
||||
private var timeFilterFrom: Long = 0
|
||||
private var timeFilterTo = Long.MAX_VALUE
|
||||
private var includeMarkedAsPlayed = false
|
||||
|
||||
private var timePlayedToday: Long = 0
|
||||
private var timeSpentToday: Long = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
loadStatistics()
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface)
|
||||
Row {
|
||||
Text(stringResource(R.string.duration) + ": " + shortLocalizedDuration(context, timePlayedToday), color = MaterialTheme.colorScheme.onSurface)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Text( stringResource(R.string.spent) + ": " + shortLocalizedDuration(context, timeSpentToday), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
val headerCaption = if (includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total)
|
||||
else {
|
||||
if (timeFilterFrom != 0L || timeFilterTo != Long.MAX_VALUE) {
|
||||
val skeleton = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM yyyy")
|
||||
val dateFormat = SimpleDateFormat(skeleton, Locale.getDefault())
|
||||
val dateFrom = dateFormat.format(Date(timeFilterFrom))
|
||||
// FilterTo is first day of next month => Subtract one day
|
||||
val dateTo = dateFormat.format(Date(timeFilterTo - 24L * 3600000L))
|
||||
stringResource(R.string.statistics_counting_range, dateFrom, dateTo)
|
||||
} else stringResource(R.string.statistics_counting_total)
|
||||
}
|
||||
Text(headerCaption, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 20.dp))
|
||||
Row {
|
||||
Text(stringResource(R.string.duration) + ": " + shortLocalizedDuration(context, lineChartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Text( stringResource(R.string.spent) + ": " + shortLocalizedDuration(context, timeSpentSum), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
HorizontalLineChart(lineChartData)
|
||||
StatsList(statisticsData, lineChartData) { item ->
|
||||
context.getString(R.string.duration) + ": " + shortLocalizedDuration(context, item!!.timePlayed) +
|
||||
"\t" + context.getString(R.string.spent) + ": " + shortLocalizedDuration(context, item.timeSpent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return composeView
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
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.StatisticsEvent -> loadStatistics()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(true)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(true)
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.statistics_filter) {
|
||||
val dialog = object: DatesFilterDialog(requireContext(), statisticsData.oldestDate) {
|
||||
override fun initParams() {
|
||||
prefs = Companion.prefs
|
||||
includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
timeFilterFrom = prefs!!.getLong(PREF_FILTER_FROM, 0)
|
||||
timeFilterTo = prefs!!.getLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
}
|
||||
override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) {
|
||||
prefs!!.edit()
|
||||
.putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed)
|
||||
.putLong(PREF_FILTER_FROM, timeFilterFrom)
|
||||
.putLong(PREF_FILTER_TO, timeFilterTo)
|
||||
.apply()
|
||||
EventFlow.postEvent(FlowEvent.StatisticsEvent())
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
private fun setTimeFilter(includeMarkedAsPlayed: Boolean, timeFilterFrom: Long, timeFilterTo: Long) {
|
||||
this.includeMarkedAsPlayed = includeMarkedAsPlayed
|
||||
this.timeFilterFrom = timeFilterFrom
|
||||
this.timeFilterTo = timeFilterTo
|
||||
}
|
||||
private fun loadStatistics() {
|
||||
val statsToday = getStatistics(true, LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(), Long.MAX_VALUE)
|
||||
for (item in statsToday.statsItems) {
|
||||
timePlayedToday += item.timePlayed
|
||||
timeSpentToday += item.timeSpent
|
||||
}
|
||||
val includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
val timeFilterFrom = prefs!!.getLong(PREF_FILTER_FROM, 0)
|
||||
val timeFilterTo = prefs!!.getLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
try {
|
||||
statisticsData = getStatistics(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo)
|
||||
statisticsData.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem -> item2.timePlayed.compareTo(item1.timePlayed) }
|
||||
val dataValues = MutableList(statisticsData.statsItems.size){0f}
|
||||
for (i in statisticsData.statsItems.indices) {
|
||||
val item = statisticsData.statsItems[i]
|
||||
dataValues[i] = item.timePlayed.toFloat()
|
||||
timeSpentSum += item.timeSpent
|
||||
}
|
||||
lineChartData = LineChartData(dataValues)
|
||||
// When "from" is "today", set it to today
|
||||
setTimeFilter(includeMarkedAsPlayed,
|
||||
max(min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), statisticsData.oldestDate.toDouble()).toLong(),
|
||||
min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong())
|
||||
} catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
}
|
||||
}
|
||||
|
||||
class MonthlyStatisticsFragment : Fragment() {
|
||||
private lateinit var monthlyStats: List<MonthlyStatisticsItem>
|
||||
private var maxDataValue = 1f
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
loadStatistics()
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
Column {
|
||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState()).padding(start = 20.dp, end = 20.dp)) { BarChart() }
|
||||
Spacer(Modifier.height(20.dp))
|
||||
MonthList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return composeView
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
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.StatisticsEvent -> loadStatistics()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun BarChart() {
|
||||
val barWidth = 40f
|
||||
val spaceBetweenBars = 16f
|
||||
Canvas(modifier = Modifier.width((monthlyStats.size * (barWidth + spaceBetweenBars)).dp).height(150.dp)) {
|
||||
// val canvasWidth = size.width
|
||||
val canvasHeight = size.height
|
||||
for (index in monthlyStats.indices) {
|
||||
val barHeight = (monthlyStats[index].timePlayed / maxDataValue) * canvasHeight // Normalize height
|
||||
Logd(TAG, "index: $index barHeight: $barHeight")
|
||||
val xOffset = spaceBetweenBars + index * (barWidth + spaceBetweenBars) // Calculate x position
|
||||
drawRect(color = Color.Cyan,
|
||||
topLeft = androidx.compose.ui.geometry.Offset(xOffset, canvasHeight - barHeight),
|
||||
size = androidx.compose.ui.geometry.Size(barWidth, barHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun MonthList() {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
itemsIndexed(monthlyStats) { index, item ->
|
||||
Row(Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
Column {
|
||||
val monthString = String.format(Locale.getDefault(), "%d-%d", monthlyStats[index].year, monthlyStats[index].month)
|
||||
Text(monthString, color = textColor, style = MaterialTheme.typography.bodyLarge.merge())
|
||||
val hoursString = stringResource(R.string.duration) + ": " + String.format(Locale.getDefault(), "%.1f ", monthlyStats[index].timePlayed / 3600000.0f) + stringResource(R.string.time_hours) +
|
||||
"\t" + stringResource(R.string.spent) + ": " + String.format(Locale.getDefault(), "%.1f ", monthlyStats[index].timeSpent / 3600000.0f) + stringResource(R.string.time_hours)
|
||||
Text(hoursString, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(true)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(false)
|
||||
}
|
||||
private fun loadStatistics() {
|
||||
try {
|
||||
monthlyStats = getMonthlyTimeStatistics()
|
||||
for (item in monthlyStats) maxDataValue = max(maxDataValue.toDouble(), item.timePlayed.toDouble()).toFloat()
|
||||
Logd(TAG, "maxDataValue: $maxDataValue")
|
||||
} catch (error: Throwable) { Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
}
|
||||
private fun getMonthlyTimeStatistics(): List<MonthlyStatisticsItem> {
|
||||
Logd(TAG, "getMonthlyTimeStatistics called")
|
||||
val includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
|
||||
val months: MutableList<MonthlyStatisticsItem> = ArrayList()
|
||||
val medias = realm.query(EpisodeMedia::class).query("lastPlayedTime > 0").find()
|
||||
val groupdMedias = medias.groupBy {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = it.lastPlayedTime
|
||||
"${calendar.get(Calendar.YEAR)}-${calendar.get(Calendar.MONTH) + 1}"
|
||||
}
|
||||
val orderedGroupedItems = groupdMedias.toList().sortedBy {
|
||||
val (key, _) = it
|
||||
val year = key.substringBefore("-").toInt()
|
||||
val month = key.substringAfter("-").toInt()
|
||||
year * 12 + month
|
||||
}.toMap()
|
||||
for (key in orderedGroupedItems.keys) {
|
||||
val medias_ = orderedGroupedItems[key] ?: continue
|
||||
val mItem = MonthlyStatisticsItem()
|
||||
mItem.year = key.substringBefore("-").toInt()
|
||||
mItem.month = key.substringAfter("-").toInt()
|
||||
var dur = 0L
|
||||
var spent = 0L
|
||||
for (m in medias_) {
|
||||
dur += if (m.playedDuration > 0) m.playedDuration
|
||||
else {
|
||||
if (includeMarkedAsPlayed) {
|
||||
if (m.playbackCompletionTime > 0 || (m.episodeOrFetch()?.playState ?: -10) >= PlayState.SKIPPED.code) m.duration
|
||||
else if (m.position > 0) m.position else 0
|
||||
} else m.position
|
||||
}
|
||||
spent += m.timeSpent
|
||||
}
|
||||
mItem.timePlayed = dur
|
||||
mItem.timeSpent = spent
|
||||
months.add(mItem)
|
||||
}
|
||||
return months
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadStatisticsFragment : Fragment() {
|
||||
private lateinit var statisticsData: StatisticsResult
|
||||
private lateinit var lineChartData: LineChartData
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
loadStatistics()
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(stringResource(R.string.total_size_downloaded_podcasts), color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 20.dp, bottom = 10.dp))
|
||||
Text(Formatter.formatShortFileSize(context, lineChartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
|
||||
HorizontalLineChart(lineChartData)
|
||||
StatsList(statisticsData, lineChartData) { item ->
|
||||
("${Formatter.formatShortFileSize(context, item!!.totalDownloadSize)} • "
|
||||
+ String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return composeView
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(false)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(false)
|
||||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
statisticsData = getStatistics(false, 0, Long.MAX_VALUE)
|
||||
statisticsData.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem -> item2.totalDownloadSize.compareTo(item1.totalDownloadSize) }
|
||||
val dataValues = MutableList(statisticsData.statsItems.size) { 0f }
|
||||
for (i in statisticsData.statsItems.indices) {
|
||||
val item = statisticsData.statsItems[i]
|
||||
dataValues[i] = item.totalDownloadSize.toFloat()
|
||||
}
|
||||
lineChartData = LineChartData(dataValues)
|
||||
}
|
||||
}
|
||||
|
||||
class LineChartData(val values: MutableList<Float>) {
|
||||
val sum: Float
|
||||
|
||||
init {
|
||||
var valueSum = 0f
|
||||
for (datum in values) valueSum += datum
|
||||
this.sum = valueSum
|
||||
}
|
||||
private fun getPercentageOfItem(index: Int): Float {
|
||||
if (sum == 0f) return 0f
|
||||
return values[index] / sum
|
||||
}
|
||||
private fun isLargeEnoughToDisplay(index: Int): Boolean {
|
||||
return getPercentageOfItem(index) > 0.04
|
||||
}
|
||||
fun getComposeColorOfItem(index: Int): Color {
|
||||
if (!isLargeEnoughToDisplay(index)) return Color.Gray
|
||||
return Color(COLOR_VALUES[index % COLOR_VALUES.size])
|
||||
}
|
||||
companion object {
|
||||
private val COLOR_VALUES = mutableListOf(-0xc88a1a, -0x1ae3dd, -0x6800, -0xda64dc, -0x63d850,
|
||||
-0xff663a, -0x22bb89, -0x995600, -0x47d1d2, -0xce9c6b,
|
||||
-0x66bb67, -0xdd5567, -0x5555ef, -0x99cc34, -0xff8c1a)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = StatisticsFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
private const val PREF_NAME: String = "StatisticsActivityPrefs"
|
||||
const val PREF_INCLUDE_MARKED_PLAYED: String = "countAll"
|
||||
const val PREF_FILTER_FROM: String = "filterFrom"
|
||||
const val PREF_FILTER_TO: String = "filterTo"
|
||||
|
||||
private const val POS_SUBSCRIPTIONS = 0
|
||||
private const val POS_YEARS = 1
|
||||
private const val POS_SPACE_TAKEN = 2
|
||||
private const val TOTAL_COUNT = 3
|
||||
|
||||
var prefs: SharedPreferences? = null
|
||||
|
||||
fun getSharedPrefs(context: Context) {
|
||||
if (prefs == null) prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizontalLineChart(lineChartData: LineChartData) {
|
||||
val data = lineChartData.values
|
||||
val total = data.sum()
|
||||
Canvas(modifier = Modifier.fillMaxWidth().height(50.dp).padding(start = 20.dp, end = 20.dp)) {
|
||||
val canvasWidth = size.width
|
||||
val canvasHeight = size.height
|
||||
val lineY = canvasHeight / 2
|
||||
var startX = 0f
|
||||
for (index in data.indices) {
|
||||
val segmentWidth = (data[index] / total) * canvasWidth
|
||||
Logd(TAG, "index: $index segmentWidth: $segmentWidth")
|
||||
drawRect(color = lineChartData.getComposeColorOfItem(index),
|
||||
topLeft = androidx.compose.ui.geometry.Offset(startX, lineY - 10),
|
||||
size = androidx.compose.ui.geometry.Size(segmentWidth, 20f))
|
||||
startX += segmentWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatsList(statisticsData: StatisticsResult, lineChartData: LineChartData, infoCB: (StatisticsItem?)->String) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
var showFeedStats by remember { mutableStateOf(false) }
|
||||
var feedId by remember { mutableLongStateOf(0L) }
|
||||
var feedTitle by remember { mutableStateOf("") }
|
||||
if (showFeedStats) FeedStatisticsDialog(feedTitle, feedId) { showFeedStats = false }
|
||||
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
itemsIndexed(statisticsData.statsItems, key = { _, item -> item.feed.id }) { index, item ->
|
||||
Row(Modifier.background(MaterialTheme.colorScheme.surface).fillMaxWidth().clickable(onClick = {
|
||||
Logd(SubscriptionsFragment.TAG, "icon clicked!")
|
||||
feedId = item.feed.id
|
||||
feedTitle = item.feed.title ?: "No title"
|
||||
showFeedStats = true
|
||||
})) {
|
||||
val imgLoc = remember(item) { item.feed.imageUrl }
|
||||
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc).memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
|
||||
contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(40.dp).height(40.dp).padding(end = 5.dp)
|
||||
)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column {
|
||||
Text(item.feed.title?:"No title", color = textColor, style = MaterialTheme.typography.bodyLarge.merge())
|
||||
Row {
|
||||
val chipColor = lineChartData.getComposeColorOfItem(index)
|
||||
Text("⬤", style = MaterialTheme.typography.bodyMedium.merge(), color = chipColor)
|
||||
Text(infoCB(item), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 2.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedStatisticsDialog(title: String, feedId: Long, onDismissRequest: () -> Unit) {
|
||||
var statisticsData: StatisticsItem? = null
|
||||
fun loadStatistics() {
|
||||
try {
|
||||
val data = getStatistics(false, 0, Long.MAX_VALUE, feedId)
|
||||
data.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem -> item2.timePlayed.compareTo(item1.timePlayed) }
|
||||
if (data.statsItems.isNotEmpty()) statisticsData = data.statsItems[0]
|
||||
} catch (error: Throwable) { error.printStackTrace() }
|
||||
}
|
||||
loadStatistics()
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
|
||||
Text(title)
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_episodes_started_total), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(String.format(Locale.getDefault(), "%d / %d", statisticsData?.episodesStarted ?: 0, statisticsData?.numEpisodes ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_length_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.durationOfStarted ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_time_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.timePlayed ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_time_spent), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.timeSpent ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_total_duration), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.time ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_episodes_on_device), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(String.format(Locale.getDefault(), "%d", statisticsData?.episodesDownloadCount ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_space_used), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(Formatter.formatShortFileSize(context, statisticsData?.totalDownloadSize ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Button(onClick = { onDismissRequest() }) {Text(stringResource(android.R.string.ok)) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = {
|
||||
MainActivityStarter(context).withOpenFeed(feedId).withAddToBackStack().start()
|
||||
onDismissRequest()
|
||||
}) {Text(stringResource(R.string.open_podcast)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -127,8 +127,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
private val playStateCodeSet = mutableSetOf<String>()
|
||||
private val ratingSort = MutableList(Rating.entries.size) { mutableStateOf(false)}
|
||||
private val ratingCodeSet = mutableSetOf<String>()
|
||||
private var downlaodedSort by mutableStateOf(false)
|
||||
private var commentedSort by mutableStateOf(false)
|
||||
private var downlaodedSortIndex by mutableStateOf(-1)
|
||||
private var commentedSortIndex by mutableStateOf(-1)
|
||||
|
||||
private var feedListFiltered = mutableStateListOf<Feed>()
|
||||
private var showFilterDialog by mutableStateOf(false)
|
||||
|
@ -402,7 +402,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun AutoDeleteHandlerDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, _) = remember { mutableStateOf("") }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
FeedAutoDeleteOptions.forEach { text ->
|
||||
|
@ -427,7 +427,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun SetAssociateQueueDialog(onDismissRequest: () -> Unit) {
|
||||
var selectedOption by remember {mutableStateOf("")}
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
queueSettingOptions.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -471,7 +471,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun SetKeepUpdateDialog(onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "")
|
||||
|
@ -493,7 +493,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||
|
@ -893,8 +893,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
appPrefs.edit().putBoolean("dateAscending", dateAscending).apply()
|
||||
appPrefs.edit().putBoolean("countAscending", countAscending).apply()
|
||||
appPrefs.edit().putInt("dateSortIndex", dateSortIndex).apply()
|
||||
appPrefs.edit().putBoolean("downlaodedSort", downlaodedSort).apply()
|
||||
appPrefs.edit().putBoolean("commentedSort", commentedSort).apply()
|
||||
appPrefs.edit().putInt("downlaodedSortIndex", downlaodedSortIndex).apply()
|
||||
appPrefs.edit().putInt("commentedSortIndex", commentedSortIndex).apply()
|
||||
sortArrays2CodeSet()
|
||||
appPrefs.edit().putStringSet("playStateCodeSet", playStateCodeSet).apply()
|
||||
appPrefs.edit().putStringSet("ratingCodeSet", ratingCodeSet).apply()
|
||||
|
@ -906,8 +906,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
dateAscending = appPrefs.getBoolean("dateAscending", true)
|
||||
countAscending = appPrefs.getBoolean("countAscending", true)
|
||||
dateSortIndex = appPrefs.getInt("dateSortIndex", 0)
|
||||
downlaodedSort = appPrefs.getBoolean("downlaodedSort", true)
|
||||
commentedSort = appPrefs.getBoolean("commentedSort", true)
|
||||
downlaodedSortIndex = appPrefs.getInt("downlaodedSortIndex", -1)
|
||||
commentedSortIndex = appPrefs.getInt("commentedSortIndex", -1)
|
||||
playStateCodeSet.clear()
|
||||
playStateCodeSet.addAll(appPrefs.getStringSet("playStateCodeSet", setOf())!!)
|
||||
ratingCodeSet.clear()
|
||||
|
@ -996,8 +996,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
ratingQueries += " rating == ${Rating.entries[i].code} "
|
||||
}
|
||||
}
|
||||
val downloadedQuery = if (downlaodedSort) " media.downloaded == true " else ""
|
||||
val commentedQuery = if (commentedSort) " comment != '' " else ""
|
||||
val downloadedQuery = if (downlaodedSortIndex == 0) " media.downloaded == true " else if (downlaodedSortIndex == 1) " media.downloaded == false " else ""
|
||||
val commentedQuery = if (commentedSortIndex == 0) " comment != '' " else if (commentedSortIndex == 1) " comment == '' " else ""
|
||||
|
||||
var queryString = "feedId == $0"
|
||||
if (playStateQueries.isNotEmpty()) queryString += " AND ($playStateQueries)"
|
||||
|
@ -1104,29 +1104,78 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
|
||||
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
|
||||
if (sortIndex == 2) {
|
||||
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (!downlaodedSort) textColor else Color.Green),
|
||||
onClick = {
|
||||
downlaodedSort = !downlaodedSort
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
}
|
||||
) { Text(stringResource(R.string.downloaded_label)) }
|
||||
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (!commentedSort) textColor else Color.Green),
|
||||
onClick = {
|
||||
commentedSort = !commentedSort
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
}
|
||||
) { Text(stringResource(R.string.commented)) }
|
||||
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
|
||||
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),
|
||||
onClick = {
|
||||
if (downlaodedSortIndex != 0) {
|
||||
selectNone = false
|
||||
downlaodedSortIndex = 0
|
||||
} else downlaodedSortIndex = -1
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
},
|
||||
) { 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),
|
||||
onClick = {
|
||||
if (downlaodedSortIndex != 1) {
|
||||
selectNone = false
|
||||
downlaodedSortIndex = 1
|
||||
} else downlaodedSortIndex = -1
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
},
|
||||
) { Text(text = stringResource(item.values[1].displayName), color = textColor) }
|
||||
Spacer(Modifier.weight(0.5f))
|
||||
}
|
||||
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
|
||||
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),
|
||||
onClick = {
|
||||
if (commentedSortIndex != 0) {
|
||||
selectNone = false
|
||||
commentedSortIndex = 0
|
||||
} else commentedSortIndex = -1
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
},
|
||||
) { 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),
|
||||
onClick = {
|
||||
if (commentedSortIndex != 1) {
|
||||
selectNone = false
|
||||
commentedSortIndex = 1
|
||||
} else commentedSortIndex = -1
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
},
|
||||
) { Text(text = stringResource(item.values[1].displayName), color = textColor) }
|
||||
Spacer(Modifier.weight(0.5f))
|
||||
}
|
||||
}
|
||||
if ((sortIndex == 1 && dateSortIndex == 0) || sortIndex == 2) {
|
||||
val item = EpisodeFilter.EpisodesFilterGroup.PLAY_STATE
|
||||
var selectNone by remember { mutableStateOf(false) }
|
||||
var expandRow by remember { mutableStateOf(false) }
|
||||
Row {
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
|
||||
expandRow = !expandRow
|
||||
})
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor,
|
||||
modifier = Modifier.clickable {
|
||||
expandRow = !expandRow
|
||||
})
|
||||
var lowerSelected by remember { mutableStateOf(false) }
|
||||
var higherSelected by remember { mutableStateOf(false) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
@ -1149,9 +1198,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
modifier = Modifier.clickable {
|
||||
lowerSelected = false
|
||||
higherSelected = false
|
||||
for (i in item.values.indices) {
|
||||
playStateSort[i].value = false
|
||||
}
|
||||
for (i in item.values.indices) playStateSort[i].value = false
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
})
|
||||
|
@ -1186,9 +1233,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
doSort()
|
||||
saveSortingPrefs()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor)
|
||||
}
|
||||
) { Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor) }
|
||||
}
|
||||
}
|
||||
if (sortIndex == 2) {
|
||||
|
@ -1196,9 +1241,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
var selectNone by remember { mutableStateOf(false) }
|
||||
var expandRow by remember { mutableStateOf(false) }
|
||||
Row {
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
|
||||
expandRow = !expandRow
|
||||
})
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor,
|
||||
modifier = Modifier.clickable {
|
||||
expandRow = !expandRow
|
||||
})
|
||||
var lowerSelected by remember { mutableStateOf(false) }
|
||||
var higherSelected by remember { mutableStateOf(false) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
@ -1221,9 +1267,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
modifier = Modifier.clickable {
|
||||
lowerSelected = false
|
||||
higherSelected = false
|
||||
for (i in item.values.indices) {
|
||||
ratingSort[i].value = false
|
||||
}
|
||||
for (i in item.values.indices) ratingSort[i].value = false
|
||||
doSort()
|
||||
saveSortingPrefs()
|
||||
})
|
||||
|
@ -1246,9 +1290,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
|
||||
if (selectNone) ratingSort[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 (ratingSort[index].value) Color.Green else textColor),
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.statistics
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import ac.mdiq.podcini.storage.model.MonthlyStatisticsItem
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
class BarChartView : AppCompatImageView {
|
||||
private var drawable: BarChartDrawable? = null
|
||||
|
||||
constructor(context: Context) : super(context!!) {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context!!, attrs) {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr) {
|
||||
setup()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setup() {
|
||||
drawable = BarChartDrawable()
|
||||
setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of data values to display.
|
||||
*/
|
||||
fun setData(data: List<MonthlyStatisticsItem>) {
|
||||
drawable!!.data = data
|
||||
drawable!!.maxValue = 1
|
||||
for (item in data) {
|
||||
drawable!!.maxValue = max(drawable!!.maxValue.toDouble(), item.timePlayed.toDouble()).toLong()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class BarChartDrawable : Drawable() {
|
||||
private val ONE_HOUR = 3600000L
|
||||
|
||||
var data: List<MonthlyStatisticsItem>? = null
|
||||
var maxValue: Long = 1
|
||||
private val paintBars: Paint
|
||||
private val paintGridLines: Paint
|
||||
private val paintGridText: Paint
|
||||
private val colors = intArrayOf(0, -0x63d850)
|
||||
|
||||
init {
|
||||
colors[0] = getColorFromAttr(context, androidx.appcompat.R.attr.colorAccent)
|
||||
paintBars = Paint()
|
||||
paintBars.style = Paint.Style.FILL
|
||||
paintBars.isAntiAlias = true
|
||||
paintGridLines = Paint()
|
||||
paintGridLines.style = Paint.Style.STROKE
|
||||
paintGridLines.setPathEffect(DashPathEffect(floatArrayOf(10f, 10f), 0f))
|
||||
paintGridLines.color =
|
||||
getColorFromAttr(context, android.R.attr.textColorSecondary)
|
||||
paintGridText = Paint()
|
||||
paintGridText.isAntiAlias = true
|
||||
paintGridText.color =
|
||||
getColorFromAttr(context, android.R.attr.textColorSecondary)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
val barHeight = height * 0.9f
|
||||
val textPadding = width * 0.05f
|
||||
val stepSize = (width - textPadding) / (data!!.size + 2)
|
||||
val textSize = height * 0.06f
|
||||
paintGridText.textSize = textSize
|
||||
|
||||
paintBars.strokeWidth = height * 0.015f
|
||||
paintBars.color = colors[0]
|
||||
var colorIndex = 0
|
||||
var lastYear = if (data!!.size > 0) data!![0].year else 0
|
||||
for (i in data!!.indices) {
|
||||
val x = textPadding + (i + 1) * stepSize
|
||||
if (lastYear != data!![i].year) {
|
||||
lastYear = data!![i].year
|
||||
colorIndex++
|
||||
paintBars.color = colors[colorIndex % 2]
|
||||
if (i < data!!.size - 2) {
|
||||
canvas.drawText(data!![i].year.toString(), x + stepSize,
|
||||
barHeight + (height - barHeight + textSize) / 2, paintGridText)
|
||||
}
|
||||
canvas.drawLine(x, height, x, barHeight, paintGridText)
|
||||
}
|
||||
|
||||
val valuePercentage = max(0.005, (data!![i].timePlayed.toFloat() / maxValue).toDouble())
|
||||
.toFloat()
|
||||
val y = (1 - valuePercentage) * barHeight
|
||||
canvas.drawRect(x, y, x + stepSize * 0.95f, barHeight, paintBars)
|
||||
}
|
||||
|
||||
val maxLine = (floor(maxValue / (10.0 * ONE_HOUR)) * 10 * ONE_HOUR).toFloat()
|
||||
var y = (1 - (maxLine / maxValue)) * barHeight
|
||||
canvas.drawLine(0f, y, width, y, paintGridLines)
|
||||
canvas.drawText((maxLine.toLong() / ONE_HOUR).toString(), 0f, y + 1.2f * textSize, paintGridText)
|
||||
|
||||
val midLine = maxLine / 2
|
||||
y = (1 - (midLine / maxValue)) * barHeight
|
||||
canvas.drawLine(0f, y, width, y, paintGridLines)
|
||||
canvas.drawText((midLine.toLong() / ONE_HOUR).toString(), 0f, y + 1.2f * textSize, paintGridText)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.statistics
|
||||
|
||||
import ac.mdiq.podcini.databinding.FeedStatisticsBinding
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats.getStatistics
|
||||
import ac.mdiq.podcini.storage.model.StatisticsItem
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
class FeedStatisticsFragment : Fragment() {
|
||||
private var _binding: FeedStatisticsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var feedId: Long = 0
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
_binding = FeedStatisticsBinding.inflate(inflater)
|
||||
if (!requireArguments().getBoolean(EXTRA_DETAILED)) {
|
||||
for (i in 0 until binding.root.childCount) {
|
||||
val child = binding.root.getChildAt(i)
|
||||
if ("detailed" == child.tag) child.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
loadStatistics()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
val data = getStatistics(true, 0, Long.MAX_VALUE)
|
||||
data.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
item2.timePlayed.compareTo(item1.timePlayed)
|
||||
}
|
||||
for (statisticsItem in data.statsItems) {
|
||||
if (statisticsItem.feed.id == feedId) return@withContext statisticsItem
|
||||
}
|
||||
null
|
||||
}
|
||||
showStats(statisticsData)
|
||||
} catch (error: Throwable) { error.printStackTrace() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStats(s: StatisticsItem?) {
|
||||
if (s == null) return
|
||||
binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s.episodesStarted, s.numEpisodes)
|
||||
binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed)
|
||||
binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time)
|
||||
binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount)
|
||||
binding.spaceUsedLabel.text = Formatter.formatShortFileSize(context, s.totalDownloadSize)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_FEED_ID = "ac.mdiq.podcini.extra.feedId"
|
||||
const val EXTRA_DETAILED = "ac.mdiq.podcini.extra.detailed"
|
||||
|
||||
fun newInstance(feedId: Long, detailed: Boolean): FeedStatisticsFragment {
|
||||
val fragment = FeedStatisticsFragment()
|
||||
val arguments = Bundle()
|
||||
arguments.putLong(EXTRA_FEED_ID, feedId)
|
||||
arguments.putBoolean(EXTRA_DETAILED, detailed)
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.statistics
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
class PieChartView : AppCompatImageView {
|
||||
private var drawable: PieChartDrawable? = null
|
||||
|
||||
constructor(context: Context) : super(context!!) {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context!!, attrs) {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr) {
|
||||
setup()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setup() {
|
||||
drawable = PieChartDrawable()
|
||||
setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of data values to display.
|
||||
*/
|
||||
fun setData(data: PieChartData?) {
|
||||
drawable!!.data = data
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = measuredWidth
|
||||
setMeasuredDimension(width, width / 2)
|
||||
}
|
||||
|
||||
class PieChartData(val values: FloatArray) {
|
||||
val sum: Float
|
||||
|
||||
init {
|
||||
var valueSum = 0f
|
||||
for (datum in values) {
|
||||
valueSum += datum
|
||||
}
|
||||
this.sum = valueSum
|
||||
}
|
||||
|
||||
fun getPercentageOfItem(index: Int): Float {
|
||||
if (sum == 0f) {
|
||||
return 0f
|
||||
}
|
||||
return values[index] / sum
|
||||
}
|
||||
|
||||
fun isLargeEnoughToDisplay(index: Int): Boolean {
|
||||
return getPercentageOfItem(index) > 0.04
|
||||
}
|
||||
|
||||
fun getColorOfItem(index: Int): Int {
|
||||
if (!isLargeEnoughToDisplay(index)) {
|
||||
return Color.GRAY
|
||||
}
|
||||
return COLOR_VALUES[index % COLOR_VALUES.size]
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COLOR_VALUES = intArrayOf(-0xc88a1a, -0x1ae3dd, -0x6800, -0xda64dc, -0x63d850,
|
||||
-0xff663a, -0x22bb89, -0x995600, -0x47d1d2, -0xce9c6b,
|
||||
-0x66bb67, -0xdd5567, -0x5555ef, -0x99cc34, -0xff8c1a)
|
||||
}
|
||||
}
|
||||
|
||||
private class PieChartDrawable : Drawable() {
|
||||
var data: PieChartData? = null
|
||||
private val paint = Paint()
|
||||
|
||||
init {
|
||||
paint.flags = Paint.ANTI_ALIAS_FLAG
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeJoin = Paint.Join.ROUND
|
||||
paint.strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val strokeSize = bounds.height() / 30f
|
||||
paint.strokeWidth = strokeSize
|
||||
|
||||
val radius = bounds.height() - strokeSize
|
||||
val center = bounds.width() / 2f
|
||||
val arcBounds = RectF(center - radius, strokeSize, center + radius, strokeSize + radius * 2)
|
||||
|
||||
var startAngle = 180f
|
||||
for (i in data!!.values.indices) {
|
||||
if (!data!!.isLargeEnoughToDisplay(i)) {
|
||||
break
|
||||
}
|
||||
paint.color = data!!.getColorOfItem(i)
|
||||
val padding = if (i == 0) PADDING_DEGREES / 2 else PADDING_DEGREES
|
||||
val sweepAngle = (180f - PADDING_DEGREES) * data!!.getPercentageOfItem(i)
|
||||
canvas.drawArc(arcBounds, startAngle + padding, sweepAngle - padding, false, paint)
|
||||
startAngle += sweepAngle
|
||||
}
|
||||
|
||||
paint.color = Color.GRAY
|
||||
val sweepAngle = 360 - startAngle - PADDING_DEGREES / 2
|
||||
if (sweepAngle > PADDING_DEGREES) {
|
||||
canvas.drawArc(arcBounds, startAngle + PADDING_DEGREES, sweepAngle - PADDING_DEGREES, false, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PADDING_DEGREES = 3f
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,751 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.statistics
|
||||
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats.getStatistics
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.update
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
|
||||
import ac.mdiq.podcini.ui.statistics.PieChartView.PieChartData
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import coil.load
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class StatisticsFragment : Fragment() {
|
||||
|
||||
private lateinit var tabLayout: TabLayout
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private var _binding: PagerFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
_binding = PagerFragmentBinding.inflate(inflater)
|
||||
viewPager = binding.viewpager
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = getString(R.string.statistics_label)
|
||||
toolbar.inflateMenu(R.menu.statistics)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
(activity as MainActivity).setupToolbarToggle(toolbar, false)
|
||||
|
||||
viewPager.adapter = PagerAdapter(this)
|
||||
// Give the TabLayout the ViewPager
|
||||
tabLayout = binding.slidingTabs
|
||||
setupPagedToolbar(toolbar, viewPager)
|
||||
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab: TabLayout.Tab, position: Int ->
|
||||
when (position) {
|
||||
POS_SUBSCRIPTIONS -> tab.setText(R.string.subscriptions_label)
|
||||
POS_YEARS -> tab.setText(R.string.years_statistics_label)
|
||||
POS_SPACE_TAKEN -> tab.setText(R.string.downloads_label)
|
||||
else -> {}
|
||||
}
|
||||
}.attach()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.statistics_reset) {
|
||||
confirmResetStatistics()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the toolbar menu if the current child fragment is visible.
|
||||
* @param child The fragment to invalidate
|
||||
*/
|
||||
fun invalidateOptionsMenuIfActive(child: Fragment) {
|
||||
val visibleChild = childFragmentManager.findFragmentByTag("f" + viewPager.currentItem)
|
||||
if (visibleChild === child) visibleChild.onPrepareOptionsMenu(toolbar.menu)
|
||||
}
|
||||
|
||||
private fun setupPagedToolbar(toolbar: MaterialToolbar, viewPager: ViewPager2) {
|
||||
this.toolbar = toolbar
|
||||
this.viewPager = viewPager
|
||||
|
||||
toolbar.setOnMenuItemClickListener { item: MenuItem? ->
|
||||
if (this.onOptionsItemSelected(item!!)) return@setOnMenuItemClickListener true
|
||||
val child = childFragmentManager.findFragmentByTag("f" + viewPager.currentItem)
|
||||
if (child != null) return@setOnMenuItemClickListener child.onOptionsItemSelected(item)
|
||||
false
|
||||
}
|
||||
viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
val child = childFragmentManager.findFragmentByTag("f$position")
|
||||
child?.onPrepareOptionsMenu(toolbar.menu)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun confirmResetStatistics() {
|
||||
val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(),
|
||||
R.string.statistics_reset_data, R.string.statistics_reset_data_msg) {
|
||||
override fun onConfirmButtonPressed(dialog: DialogInterface) {
|
||||
dialog.dismiss()
|
||||
doResetStatistics()
|
||||
}
|
||||
}
|
||||
conDialog.createNewDialog().show()
|
||||
}
|
||||
|
||||
private fun doResetStatistics() {
|
||||
prefs!!.edit()
|
||||
.putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
.putLong(PREF_FILTER_FROM, 0)
|
||||
.putLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
.apply()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
resetStatistics()
|
||||
}
|
||||
// This runs on the Main thread
|
||||
EventFlow.postEvent(FlowEvent.StatisticsEvent())
|
||||
} catch (error: Throwable) {
|
||||
// This also runs on the Main thread
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetStatistics(): Job {
|
||||
return runOnIOScope {
|
||||
val mediaAll = realm.query(EpisodeMedia::class).find()
|
||||
for (m in mediaAll) {
|
||||
update(m) { m.playedDuration = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
POS_SUBSCRIPTIONS -> SubscriptionStatisticsFragment()
|
||||
POS_YEARS -> YearsStatisticsFragment()
|
||||
POS_SPACE_TAKEN -> DownloadStatisticsFragment()
|
||||
else -> DownloadStatisticsFragment()
|
||||
}
|
||||
}
|
||||
override fun getItemCount(): Int {
|
||||
return TOTAL_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
class SubscriptionStatisticsFragment : Fragment() {
|
||||
private var _binding: StatisticsFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var statisticsResult: StatisticsResult? = null
|
||||
private lateinit var feedStatisticsList: RecyclerView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var listAdapter: ListAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = StatisticsFragmentBinding.inflate(inflater)
|
||||
feedStatisticsList = binding.statisticsList
|
||||
progressBar = binding.progressBar
|
||||
listAdapter = ListAdapter(this)
|
||||
feedStatisticsList.layoutManager = LinearLayoutManager(context)
|
||||
feedStatisticsList.adapter = listAdapter
|
||||
refreshStatistics()
|
||||
return binding.root
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
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.StatisticsEvent -> refreshStatistics()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(true)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(true)
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.statistics_filter) {
|
||||
if (statisticsResult != null) {
|
||||
val dialog = object: DatesFilterDialog(requireContext(), statisticsResult!!.oldestDate) {
|
||||
override fun initParams() {
|
||||
prefs = StatisticsFragment.prefs
|
||||
includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
timeFilterFrom = prefs!!.getLong(PREF_FILTER_FROM, 0)
|
||||
timeFilterTo = prefs!!.getLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
}
|
||||
override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) {
|
||||
prefs!!.edit()
|
||||
.putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed)
|
||||
.putLong(PREF_FILTER_FROM, timeFilterFrom)
|
||||
.putLong(PREF_FILTER_TO, timeFilterTo)
|
||||
.apply()
|
||||
EventFlow.postEvent(FlowEvent.StatisticsEvent())
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
private fun refreshStatistics() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
feedStatisticsList.visibility = View.GONE
|
||||
loadStatistics()
|
||||
}
|
||||
private fun loadStatistics() {
|
||||
val includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
val timeFilterFrom = prefs!!.getLong(PREF_FILTER_FROM, 0)
|
||||
val timeFilterTo = prefs!!.getLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
val data = getStatistics(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo)
|
||||
data.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
item2.timePlayed.compareTo(item1.timePlayed)
|
||||
}
|
||||
data
|
||||
}
|
||||
statisticsResult = statisticsData
|
||||
// When "from" is "today", set it to today
|
||||
listAdapter.setTimeFilter(includeMarkedAsPlayed,
|
||||
max(min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), statisticsData.oldestDate.toDouble()).toLong(),
|
||||
min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong())
|
||||
listAdapter.update(statisticsData.statsItems)
|
||||
progressBar.visibility = View.GONE
|
||||
feedStatisticsList.visibility = View.VISIBLE
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ListAdapter(private val fragment: Fragment) : StatisticsListAdapter(fragment.requireContext()) {
|
||||
private var timeFilterFrom: Long = 0
|
||||
private var timeFilterTo = Long.MAX_VALUE
|
||||
private var includeMarkedAsPlayed = false
|
||||
|
||||
override val headerCaption: String
|
||||
get() {
|
||||
if (includeMarkedAsPlayed) return context.getString(R.string.statistics_counting_total)
|
||||
val skeleton = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM yyyy")
|
||||
val dateFormat = SimpleDateFormat(skeleton, Locale.getDefault())
|
||||
val dateFrom = dateFormat.format(Date(timeFilterFrom))
|
||||
// FilterTo is first day of next month => Subtract one day
|
||||
val dateTo = dateFormat.format(Date(timeFilterTo - 24L * 3600000L))
|
||||
return context.getString(R.string.statistics_counting_range, dateFrom, dateTo)
|
||||
}
|
||||
override val headerValue: String
|
||||
get() = shortLocalizedDuration(context, pieChartData!!.sum.toLong())
|
||||
|
||||
fun setTimeFilter(includeMarkedAsPlayed: Boolean, timeFilterFrom: Long, timeFilterTo: Long) {
|
||||
this.includeMarkedAsPlayed = includeMarkedAsPlayed
|
||||
this.timeFilterFrom = timeFilterFrom
|
||||
this.timeFilterTo = timeFilterTo
|
||||
}
|
||||
override fun generateChartData(statisticsData: List<StatisticsItem>?): PieChartData {
|
||||
val dataValues = FloatArray(statisticsData!!.size)
|
||||
for (i in statisticsData.indices) {
|
||||
val item = statisticsData[i]
|
||||
dataValues[i] = item.timePlayed.toFloat()
|
||||
}
|
||||
return PieChartData(dataValues)
|
||||
}
|
||||
override fun onBindFeedViewHolder(holder: StatisticsHolder?, item: StatisticsItem?) {
|
||||
val time = item!!.timePlayed
|
||||
holder!!.value.text = shortLocalizedDuration(context, time)
|
||||
holder.itemView.setOnClickListener {
|
||||
val yourDialogFragment = StatisticsDialogFragment.newInstance(item.feed.id, item.feed.title)
|
||||
yourDialogFragment.show(fragment.childFragmentManager.beginTransaction(), "DialogFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class YearsStatisticsFragment : Fragment() {
|
||||
private var _binding: StatisticsFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var yearStatisticsList: RecyclerView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var listAdapter: ListAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = StatisticsFragmentBinding.inflate(inflater)
|
||||
yearStatisticsList = binding.statisticsList
|
||||
progressBar = binding.progressBar
|
||||
listAdapter = ListAdapter(requireContext())
|
||||
yearStatisticsList.layoutManager = LinearLayoutManager(context)
|
||||
yearStatisticsList.adapter = listAdapter
|
||||
refreshStatistics()
|
||||
return binding.root
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
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.StatisticsEvent -> refreshStatistics()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(true)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(false)
|
||||
}
|
||||
private fun refreshStatistics() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
yearStatisticsList.visibility = View.GONE
|
||||
loadStatistics()
|
||||
}
|
||||
private fun loadStatistics() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result: List<MonthlyStatisticsItem> = withContext(Dispatchers.IO) {
|
||||
getMonthlyTimeStatistics()
|
||||
}
|
||||
listAdapter.update(result)
|
||||
progressBar.visibility = View.GONE
|
||||
yearStatisticsList.visibility = View.VISIBLE
|
||||
} catch (error: Throwable) {
|
||||
// This also runs on the Main thread
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun getMonthlyTimeStatistics(): List<MonthlyStatisticsItem> {
|
||||
Logd(TAG, "getMonthlyTimeStatistics called")
|
||||
val includeMarkedAsPlayed = prefs!!.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
|
||||
val months: MutableList<MonthlyStatisticsItem> = ArrayList()
|
||||
val medias = realm.query(EpisodeMedia::class).query("lastPlayedTime > 0").find()
|
||||
val groupdMedias = medias.groupBy {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = it.lastPlayedTime
|
||||
"${calendar.get(Calendar.YEAR)}-${calendar.get(Calendar.MONTH) + 1}"
|
||||
}
|
||||
val orderedGroupedItems = groupdMedias.toList().sortedBy {
|
||||
val (key, _) = it
|
||||
val year = key.substringBefore("-").toInt()
|
||||
val month = key.substringAfter("-").toInt()
|
||||
year * 12 + month
|
||||
}.toMap()
|
||||
for (key in orderedGroupedItems.keys) {
|
||||
val medias_ = orderedGroupedItems[key] ?: continue
|
||||
val mItem = MonthlyStatisticsItem()
|
||||
mItem.year = key.substringBefore("-").toInt()
|
||||
mItem.month = key.substringAfter("-").toInt()
|
||||
var dur = 0L
|
||||
for (m in medias_) {
|
||||
if (m.playedDuration > 0) dur += m.playedDuration
|
||||
else {
|
||||
// progress import does not include playedDuration
|
||||
if (includeMarkedAsPlayed) {
|
||||
if (m.playbackCompletionTime > 0 || (m.episodeOrFetch()?.playState?:-10) >= PlayState.SKIPPED.code)
|
||||
dur += m.duration
|
||||
else if (m.position > 0) dur += m.position
|
||||
} else dur += m.position
|
||||
}
|
||||
}
|
||||
mItem.timePlayed = dur
|
||||
months.add(mItem)
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter for the yearly playback statistics list.
|
||||
*/
|
||||
private class ListAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val statisticsData: MutableList<MonthlyStatisticsItem> = ArrayList()
|
||||
private val yearlyAggregate: MutableList<MonthlyStatisticsItem?> = ArrayList()
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return yearlyAggregate.size + 1
|
||||
}
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0) TYPE_HEADER else TYPE_FEED
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
if (viewType == TYPE_HEADER) return HeaderHolder(inflater.inflate(R.layout.statistics_listitem_barchart, parent, false))
|
||||
return StatisticsHolder(inflater.inflate(R.layout.statistics_year_listitem, parent, false))
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(h: RecyclerView.ViewHolder, position: Int) {
|
||||
if (getItemViewType(position) == TYPE_HEADER) {
|
||||
val holder = h as HeaderHolder
|
||||
holder.barChart.setData(statisticsData)
|
||||
} else {
|
||||
val holder = h as StatisticsHolder
|
||||
val statsItem = yearlyAggregate[position - 1]
|
||||
holder.year.text = String.format(Locale.getDefault(), "%d ", statsItem!!.year)
|
||||
holder.hours.text = String.format(Locale.getDefault(),
|
||||
"%.1f ",
|
||||
statsItem.timePlayed / 3600000.0f) + context.getString(R.string.time_hours)
|
||||
}
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun update(statistics: List<MonthlyStatisticsItem>) {
|
||||
var lastYear = if (statistics.isNotEmpty()) statistics[0].year else 0
|
||||
var lastDataPoint = if (statistics.isNotEmpty()) (statistics[0].month - 1) + lastYear * 12 else 0
|
||||
var yearSum: Long = 0
|
||||
yearlyAggregate.clear()
|
||||
statisticsData.clear()
|
||||
for (statistic in statistics) {
|
||||
if (statistic.year != lastYear) {
|
||||
val yearAggregate = MonthlyStatisticsItem()
|
||||
yearAggregate.year = lastYear
|
||||
yearAggregate.timePlayed = yearSum
|
||||
yearlyAggregate.add(yearAggregate)
|
||||
yearSum = 0
|
||||
lastYear = statistic.year
|
||||
}
|
||||
yearSum += statistic.timePlayed
|
||||
while (lastDataPoint + 1 < (statistic.month - 1) + statistic.year * 12) {
|
||||
lastDataPoint++
|
||||
val item = MonthlyStatisticsItem()
|
||||
item.year = lastDataPoint / 12
|
||||
item.month = lastDataPoint % 12 + 1
|
||||
statisticsData.add(item) // Compensate for months without playback
|
||||
}
|
||||
statisticsData.add(statistic)
|
||||
lastDataPoint = (statistic.month - 1) + statistic.year * 12
|
||||
}
|
||||
val yearAggregate = MonthlyStatisticsItem()
|
||||
yearAggregate.year = lastYear
|
||||
yearAggregate.timePlayed = yearSum
|
||||
yearlyAggregate.add(yearAggregate)
|
||||
yearlyAggregate.reverse()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private class HeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = StatisticsListitemBarchartBinding.bind(itemView)
|
||||
var barChart: BarChartView = binding.barChart
|
||||
}
|
||||
|
||||
private class StatisticsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = StatisticsYearListitemBinding.bind(itemView)
|
||||
var year: TextView = binding.yearLabel
|
||||
var hours: TextView = binding.hoursLabel
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER = 0
|
||||
private const val TYPE_FEED = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadStatisticsFragment : Fragment() {
|
||||
private var _binding: StatisticsFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var downloadStatisticsList: RecyclerView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var listAdapter: ListAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = StatisticsFragmentBinding.inflate(inflater)
|
||||
downloadStatisticsList = binding.statisticsList
|
||||
progressBar = binding.progressBar
|
||||
listAdapter = ListAdapter(requireContext(), this)
|
||||
downloadStatisticsList.layoutManager = LinearLayoutManager(context)
|
||||
downloadStatisticsList.adapter = listAdapter
|
||||
refreshDownloadStatistics()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.statistics_reset).setVisible(false)
|
||||
menu.findItem(R.id.statistics_filter).setVisible(false)
|
||||
}
|
||||
private fun refreshDownloadStatistics() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
downloadStatisticsList.visibility = View.GONE
|
||||
loadStatistics()
|
||||
}
|
||||
private fun loadStatistics() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
val data = getStatistics(false, 0, Long.MAX_VALUE)
|
||||
data.statsItems.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
item2.totalDownloadSize.compareTo(item1.totalDownloadSize)
|
||||
}
|
||||
data
|
||||
}
|
||||
listAdapter.update(statisticsData.statsItems)
|
||||
progressBar.visibility = View.GONE
|
||||
downloadStatisticsList.visibility = View.VISIBLE
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ListAdapter(context: Context, private val fragment: Fragment) : StatisticsListAdapter(context) {
|
||||
override val headerCaption: String
|
||||
get() = context.getString(R.string.total_size_downloaded_podcasts)
|
||||
override val headerValue: String
|
||||
get() = Formatter.formatShortFileSize(context, pieChartData!!.sum.toLong())
|
||||
|
||||
override fun generateChartData(statisticsData: List<StatisticsItem>?): PieChartData {
|
||||
val dataValues = FloatArray(statisticsData!!.size)
|
||||
for (i in statisticsData.indices) {
|
||||
val item = statisticsData[i]
|
||||
dataValues[i] = item.totalDownloadSize.toFloat()
|
||||
}
|
||||
return PieChartData(dataValues)
|
||||
}
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindFeedViewHolder(holder: StatisticsHolder?, item: StatisticsItem?) {
|
||||
holder!!.value.text = ("${Formatter.formatShortFileSize(context, item!!.totalDownloadSize)} • "
|
||||
+ String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
|
||||
holder.itemView.setOnClickListener {
|
||||
val yourDialogFragment = StatisticsDialogFragment.newInstance(item.feed.id, item.feed.title)
|
||||
yourDialogFragment.show(fragment.childFragmentManager.beginTransaction(), "DialogFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StatisticsDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
dialog.setPositiveButton(android.R.string.ok, null)
|
||||
dialog.setNeutralButton(R.string.open_podcast) { _: DialogInterface?, _: Int ->
|
||||
val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
MainActivityStarter(requireContext()).withOpenFeed(feedId).withAddToBackStack().start()
|
||||
}
|
||||
dialog.setTitle(requireArguments().getString(EXTRA_FEED_TITLE))
|
||||
dialog.setView(R.layout.feed_statistics_dialog)
|
||||
return dialog.create()
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
childFragmentManager.beginTransaction().replace(R.id.statisticsContainer,
|
||||
FeedStatisticsFragment.newInstance(feedId, true), "feed_statistics_fragment")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_FEED_ID = "ac.mdiq.podcini.extra.feedId"
|
||||
private const val EXTRA_FEED_TITLE = "ac.mdiq.podcini.extra.feedTitle"
|
||||
|
||||
fun newInstance(feedId: Long, feedTitle: String?): StatisticsDialogFragment {
|
||||
val fragment = StatisticsDialogFragment()
|
||||
val arguments = Bundle()
|
||||
arguments.putLong(EXTRA_FEED_ID, feedId)
|
||||
arguments.putString(EXTRA_FEED_TITLE, feedTitle)
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent Adapter for the playback and download statistics list.
|
||||
*/
|
||||
private abstract class StatisticsListAdapter protected constructor(@JvmField protected val context: Context) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var statisticsData: List<StatisticsItem>? = null
|
||||
@JvmField
|
||||
protected var pieChartData: PieChartData? = null
|
||||
protected abstract val headerCaption: String?
|
||||
protected abstract val headerValue: String?
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return statisticsData!!.size + 1
|
||||
}
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0) TYPE_HEADER else TYPE_FEED
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
if (viewType == TYPE_HEADER) return HeaderHolder(inflater.inflate(R.layout.statistics_listitem_total, parent, false))
|
||||
return StatisticsHolder(inflater.inflate(R.layout.statistics_listitem, parent, false))
|
||||
}
|
||||
override fun onBindViewHolder(h: RecyclerView.ViewHolder, position: Int) {
|
||||
if (getItemViewType(position) == TYPE_HEADER) {
|
||||
val holder = h as HeaderHolder
|
||||
holder.pieChart.setData(pieChartData)
|
||||
holder.totalTime.text = headerValue
|
||||
holder.totalText.text = headerCaption
|
||||
} else {
|
||||
val holder = h as StatisticsHolder
|
||||
val statsItem = statisticsData!![position - 1]
|
||||
holder.image.load(statsItem.feed.imageUrl) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
}
|
||||
holder.title.text = statsItem.feed.title
|
||||
holder.chip.setTextColor(pieChartData!!.getColorOfItem(position - 1))
|
||||
onBindFeedViewHolder(holder, statsItem)
|
||||
}
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun update(statistics: List<StatisticsItem>?) {
|
||||
statisticsData = statistics
|
||||
pieChartData = generateChartData(statistics)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class HeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = StatisticsListitemTotalBinding.bind(itemView)
|
||||
var totalTime: TextView = binding.totalTime
|
||||
var pieChart: PieChartView = binding.pieChart
|
||||
var totalText: TextView = binding.totalDescription
|
||||
}
|
||||
|
||||
class StatisticsHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = StatisticsListitemBinding.bind(itemView)
|
||||
var image: ImageView = binding.imgvCover
|
||||
var title: TextView = binding.txtvTitle
|
||||
@JvmField
|
||||
var value: TextView = binding.txtvValue
|
||||
var chip: TextView = binding.chip
|
||||
}
|
||||
|
||||
protected abstract fun generateChartData(statisticsData: List<StatisticsItem>?): PieChartData?
|
||||
protected abstract fun onBindFeedViewHolder(holder: StatisticsHolder?, item: StatisticsItem?)
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER = 0
|
||||
private const val TYPE_FEED = 1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = StatisticsFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
const val PREF_NAME: String = "StatisticsActivityPrefs"
|
||||
const val PREF_INCLUDE_MARKED_PLAYED: String = "countAll"
|
||||
const val PREF_FILTER_FROM: String = "filterFrom"
|
||||
const val PREF_FILTER_TO: String = "filterTo"
|
||||
|
||||
private const val POS_SUBSCRIPTIONS = 0
|
||||
private const val POS_YEARS = 1
|
||||
private const val POS_SPACE_TAKEN = 2
|
||||
private const val TOTAL_COUNT = 3
|
||||
|
||||
var prefs: SharedPreferences? = null
|
||||
|
||||
fun getSharedPrefs(context: Context) {
|
||||
if (prefs == null) prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,14 +19,12 @@ object MiscFormatter {
|
|||
@JvmStatic
|
||||
fun formatAbbrev(context: Context?, date: Date?): String {
|
||||
if (date == null) return ""
|
||||
|
||||
val now = GregorianCalendar()
|
||||
val cal = GregorianCalendar()
|
||||
cal.time = date
|
||||
val withinLastYear = now[Calendar.YEAR] == cal[Calendar.YEAR]
|
||||
var format = DateUtils.FORMAT_ABBREV_ALL
|
||||
if (withinLastYear) format = format or DateUtils.FORMAT_NO_YEAR
|
||||
|
||||
return DateUtils.formatDateTime(context, date.time, format)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TableLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TableRow
|
||||
android:tag="detailed">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/statistics_episodes_started_total" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/startedTotalLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/wait_icon"
|
||||
tools:text="0 / 0" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/statistics_time_played" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/timePlayedLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/wait_icon"
|
||||
tools:text="0 min" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:tag="detailed">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/statistics_total_duration" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/totalDurationLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/wait_icon"
|
||||
tools:text="0 min" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/statistics_episodes_on_device" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/onDeviceLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/wait_icon"
|
||||
tools:text="0" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/statistics_space_used" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/spaceUsedLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/wait_icon"
|
||||
tools:text="0 MB" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/statisticsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp" />
|
|
@ -19,7 +19,6 @@
|
|||
android:layout_height="?android:attr/actionBarSize"
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/statistics_fragment"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/statistics_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/list_vertical_padding"
|
||||
android:paddingTop="@dimen/list_vertical_padding"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
tools:listitem="@layout/statistics_listitem"/>
|
||||
|
||||
</FrameLayout>
|
|
@ -1,72 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/statistics_listitem"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:adjustViewBounds="true"
|
||||
android:cropToPadding="true"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/avatars"
|
||||
tools:background="@android:color/holo_green_dark"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvTitle"
|
||||
android:lines="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_toRightOf="@id/imgvCover"
|
||||
android:layout_toEndOf="@id/imgvCover"
|
||||
android:layout_alignTop="@id/imgvCover"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
tools:text="Feed title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:layout_toEndOf="@+id/imgvCover"
|
||||
android:layout_toRightOf="@+id/imgvCover"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_below="@+id/txtvTitle"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:text="⬤"
|
||||
tools:ignore="HardcodedText"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
android:textSize="14sp"
|
||||
android:layout_toEndOf="@+id/chip"
|
||||
android:layout_toRightOf="@+id/chip"
|
||||
android:layout_below="@+id/txtvTitle"
|
||||
tools:text="23 hours"/>
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/statistics_listitem_barchart"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ac.mdiq.podcini.ui.statistics.BarChartView
|
||||
android:id="@+id/barChart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?android:attr/dividerVertical" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,49 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/statistics_listitem_total"
|
||||
android:padding="16dp">
|
||||
|
||||
<ac.mdiq.podcini.ui.statistics.PieChartView
|
||||
android:id="@+id/pie_chart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:maxWidth="800dp"
|
||||
android:minWidth="460dp"
|
||||
android:layout_marginLeft="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:gravity="center_horizontal"
|
||||
android:textSize="28sp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_above="@id/total_description"
|
||||
tools:text="10.0 hours" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textAlignment="center"
|
||||
android:maxLines="3"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_alignBottom="@id/pie_chart" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?android:attr/dividerVertical"
|
||||
android:layout_below="@+id/pie_chart" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/statistics_year_listitem"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/yearLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
tools:text="2020" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hoursLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
android:textSize="14sp"
|
||||
tools:text="23 hours" />
|
||||
|
||||
</LinearLayout>
|
|
@ -17,13 +17,13 @@
|
|||
<string name="settings_label">Settings</string>
|
||||
<string name="downloads_label">Downloads</string>
|
||||
<string name="logs_label">Logs</string>
|
||||
|
||||
|
||||
|
||||
<string name="subscriptions_label">Subscriptions</string>
|
||||
|
||||
<string name="cancel_download_label">Cancel download</string>
|
||||
<string name="playback_history_label">History</string>
|
||||
|
||||
<string name="months_statistics_label">Months</string>
|
||||
|
||||
<string name="years_statistics_label">Years</string>
|
||||
<string name="notification_pref_fragment">Notifications</string>
|
||||
|
@ -386,6 +386,8 @@
|
|||
<string name="last_played_date">Played date</string>
|
||||
<string name="completed_date">Completed date</string>
|
||||
<string name="duration">Duration</string>
|
||||
<string name="spent">Spent</string>
|
||||
<string name="hours">hours</string>
|
||||
<string name="episode_title">Episode title</string>
|
||||
<string name="feed_title">Podcast title</string>
|
||||
<string name="show_criteria">Show criteria</string>
|
||||
|
@ -833,12 +835,15 @@
|
|||
<string name="audo_add_new_queue">Auto add new to queue</string>
|
||||
<string name="audo_add_new_queue_summary">Automatically add new episodes in this podcast to the designated queue when (auto-)refreshing</string>
|
||||
<string name="auto_download_disabled_globally">Auto download is disabled in the main Podcini settings</string>
|
||||
<string name="statistics_length_played">Duration of all started:</string>
|
||||
<string name="statistics_time_played">Time played:</string>
|
||||
<string name="statistics_time_spent">Time spent:</string>
|
||||
<string name="statistics_total_duration">Total duration (estimate):</string>
|
||||
<string name="statistics_episodes_on_device">Episodes on the device:</string>
|
||||
<string name="statistics_space_used">Space used:</string>
|
||||
<string name="statistics_episodes_started_total">Episodes started/total:</string>
|
||||
<string name="statistics_view_all">View for all podcasts »</string>
|
||||
<string name="statistics_view_this">View this podcast»</string>
|
||||
<string name="statistics_view_all">View for all podcasts»</string>
|
||||
<string name="feeds_related_to_author">Feeds likely related to the author »</string>
|
||||
<string name="wait_icon" translatable="false">{faw_spinner}</string>
|
||||
<string name="edit_url_menu">Edit feed URL</string>
|
||||
|
|
15
changelog.md
15
changelog.md
|
@ -1,3 +1,18 @@
|
|||
# 6.13.8
|
||||
|
||||
* Subscriptions sorting added the negative sides of downloaded and commented
|
||||
* added border to Compose dialogs
|
||||
* in EpisodeMedia added timeSpent to measure the actual time spent playing the episode
|
||||
* upon migration, it's set the same as playedDuration, but it will get its own value when any episode is played
|
||||
* redid and enhanced Statistics, it's in Compose
|
||||
* pie chart is replaced with line chart
|
||||
* in Subscriptions, in the header and every feed, added timeSpent
|
||||
* in Subscriptions header, added usage of today
|
||||
* on the popup of a feed statistics, also added "duration of all started"
|
||||
* the "Years" tab is now "Months", showing played time for every month, and added timeSpent for every month
|
||||
* enhanced efficiency of getting statistics for single feed
|
||||
* in FeedInfo details, added "view statistics of this feed" together with "view statistics of all feeds"
|
||||
|
||||
# 6.13.7
|
||||
|
||||
* likely fixed getting duplicate episodes on updates (youtube feeds)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
Version 6.13.8
|
||||
|
||||
* Subscriptions sorting added the negative sides of downloaded and commented
|
||||
* added border to Compose dialogs
|
||||
* in EpisodeMedia added timeSpent to measure the actual time spent playing the episode
|
||||
* upon migration, it's set the same as playedDuration, but it will get its own value when any episode is played
|
||||
* redid and enhanced Statistics, it's in Compose
|
||||
* pie chart is replaced with line chart
|
||||
* in Subscriptions, in the header and every feed, added timeSpent
|
||||
* in Subscriptions header, added usage of today
|
||||
* on the popup of a feed statistics, also added "duration of all started"
|
||||
* the "Years" tab is now "Months", showing played time for every month, and added timeSpent for every month
|
||||
* enhanced efficiency of getting statistics for single feed
|
||||
* in FeedInfo details, added "view statistics of this feed" together with "view statistics of all feeds"
|
|
@ -14,7 +14,7 @@ coordinatorlayout = "1.2.0"
|
|||
coreKtx = "1.15.0"
|
||||
coreKtxVersion = "1.8.1"
|
||||
coreSplashscreen = "1.0.1"
|
||||
desugar_jdk_libs_nio = "2.1.2"
|
||||
desugar_jdk_libs_nio = "2.1.3"
|
||||
documentfile = "1.0.1"
|
||||
fyydlin = "v0.5.0"
|
||||
googleMaterialTypeface = "4.0.0.3-kotlin"
|
||||
|
|
Loading…
Reference in New Issue