In-Memory Caching for smoother performance, Donation Dialog, Dep Update

This commit is contained in:
shabinder 2021-05-06 21:00:41 +05:30
parent 661dd1ada4
commit 60b2f04780
28 changed files with 730 additions and 34 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ build/
.gradle/
terraform.tfvars
.terraform/
/spotiflyer-ios/Pods/

View File

@ -75,7 +75,7 @@ android {
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled = true
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
@ -132,7 +132,7 @@ dependencies {
implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
implementation("com.google.accompanist:accompanist-insets:0.8.1")
implementation("com.google.accompanist:accompanist-insets:0.9.0")
// Test
testImplementation("junit:junit:4.13.2")

View File

@ -32,7 +32,7 @@ repositories {
}
dependencies {
implementation("com.android.tools.build:gradle:4.0.2")
implementation("com.android.tools.build:gradle:4.2.0")
implementation("com.google.gms:google-services:4.3.5")
implementation("com.google.firebase:perf-plugin:1.3.5")
implementation("com.google.firebase:firebase-crashlytics-gradle:2.5.2")

View File

@ -3,5 +3,5 @@ plugins {
}
repositories {
jcenter()
mavenCentral()
}

View File

@ -106,7 +106,7 @@ object Decompose {
const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION"
}
object MVIKotlin {
private const val VERSION = "2.0.2"
private const val VERSION = "2.0.3"
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION"
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION"

View File

@ -78,6 +78,7 @@ kotlin {
implementation(Extras.kermit)
implementation("co.touchlab:stately-common:1.1.6")
implementation("dev.icerock.moko:parcelize:0.6.1")
// implementation("io.github.reactivecircus.cache4k:cache4k:0.2.0-SNAPSHOT") // Local Maven
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt") {
isForce = true

View File

@ -19,6 +19,7 @@
package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -129,6 +130,12 @@ actual fun YoutubeMusicLogo() = painterResource(R.drawable.ic_youtube_music_logo
@Composable
actual fun GithubLogo() = painterResource(R.drawable.ic_github)
@Composable
actual fun PaypalLogo() = painterResource(R.drawable.ic_paypal_logo)
@Composable
actual fun RazorPay() = painterResource(R.drawable.ic_indian_rupee)
@Composable
actual fun Toast(
text: String,

View File

@ -0,0 +1,141 @@
package com.shabinder.common.uikit
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.AlertDialog
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CardGiftcard
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.shabinder.common.models.methods
import com.shabinder.common.uikit.PaypalLogo
import com.shabinder.common.uikit.RazorPay
import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorAccent
@OptIn(ExperimentalAnimationApi::class)
@Composable
actual fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
){
AnimatedVisibility(
isVisible
) {
Dialog(onDismiss) {
Card(
modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, Color.Gray) // Gray
) {
Column(Modifier.padding(16.dp)) {
Text(
"Support Us",
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center,
color = colorAccent,
modifier = Modifier
)
Spacer(modifier = Modifier.padding(vertical = 4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(
onClick = {
onDismiss()
methods.value.openPlatform("", "https://www.paypal.com/paypalme/shabinder")
}
)
.padding(vertical = 6.dp)
) {
Icon(PaypalLogo(), "Paypal Logo", tint = Color(0xFFCCCCCC))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = "Paypal",
style = SpotiFlyerTypography.h6
)
Text(
text = "International Donations (Outside India).",
style = SpotiFlyerTypography.subtitle2
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
.clickable(onClick = {
onDismiss()
methods.value.giveDonation()
}),
verticalAlignment = Alignment.CenterVertically
) {
Icon(RazorPay(), "Indian Rupee Logo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = "RazorPay",
style = SpotiFlyerTypography.h6
)
Text(
text = "Indian Donations (UPI / PayTM / PhonePe / Cards).",
style = SpotiFlyerTypography.subtitle2
)
}
}
}
}
}
/*AlertDialog(
buttons = {
*//* TextButton({
//Retry Network Connection
},
Modifier.padding(bottom = 16.dp,start = 16.dp,end = 16.dp).fillMaxWidth().background(Color(0xFFFC5C7D),shape = RoundedCornerShape(size = 8.dp)).padding(horizontal = 8.dp),
){
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
}
*//*},
*//*title = {
Column {
Text(
"Support Us",
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center,
color = colorAccent,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.padding(vertical = 16.dp))
}
},*//*
backgroundColor = Color.DarkGray,
text = {
}
,shape = SpotiFlyerShapes.medium,
onDismissRequest = onDismiss
)*/
}
}

View File

@ -1,17 +0,0 @@
/*
* * Copyright (c) 2021 Shabinder Singh
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.uikit

View File

@ -61,6 +61,12 @@ expect fun YoutubeMusicLogo(): Painter
@Composable
expect fun GithubLogo(): Painter
@Composable
expect fun PaypalLogo(): Painter
@Composable
expect fun RazorPay(): Painter
@Composable
expect fun HeartIcon(): Painter
@ -69,3 +75,9 @@ expect fun DownloadImageError()
@Composable
expect fun DownloadImageArrow(modifier: Modifier)
@Composable
expect fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
)

View File

@ -59,6 +59,9 @@ import androidx.compose.material.icons.rounded.Flag
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -318,9 +321,18 @@ fun AboutColumn(modifier: Modifier = Modifier) {
)
}
}
var isDonationDialogVisible by remember { mutableStateOf(false) }
DonationDialog(
isDonationDialogVisible
) {
isDonationDialogVisible = false
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { methods.value.giveDonation() }),
.clickable(onClick = { isDonationDialogVisible = true }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
@ -331,7 +343,8 @@ fun AboutColumn(modifier: Modifier = Modifier) {
style = SpotiFlyerTypography.h6
)
Text(
text = "If you think I deserve to get paid for my work, you can leave me some money here.",
text = "If you think I deserve to get paid for my work, you can support me here.",
//text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.",
style = SpotiFlyerTypography.subtitle2
)
}

View File

@ -0,0 +1,2 @@
package com.shabinder.common.uikit.dialogs

View File

@ -67,6 +67,14 @@ actual fun DownloadImageTick() {
)
}
@Composable
actual fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
){
}
actual fun montserratFont() = FontFamily(
Font("font/montserrat_light.ttf", FontWeight.Light),
Font("font/montserrat_regular.ttf", FontWeight.Normal),
@ -125,3 +133,9 @@ actual fun YoutubeMusicLogo() = rememberVectorPainter(vectorXmlResource("drawabl
@Composable
actual fun GithubLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_github.xml")) as Painter
@Composable
actual fun PaypalLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_paypal_logo.xml")) as Painter
@Composable
actual fun RazorPay() = rememberVectorPainter(vectorXmlResource("drawable/ic_indian_rupee.xml")) as Painter

View File

@ -21,10 +21,29 @@ plugins {
kotlin("plugin.serialization")
}
val statelyVersion = "1.1.6"
val statelyIsoVersion = "1.1.6-a1"
kotlin {
sourceSets {
/*
* Depend on https://github.com/ReactiveCircus/cache4k
* -As Soon as Kotlin 1.5 and Compose becomes compatible
* */
all {
languageSettings.apply {
progressiveMode = true
enableLanguageFeature("NewInference")
useExperimentalAnnotation("kotlin.Experimental")
useExperimentalAnnotation("kotlin.time.ExperimentalTime")
}
}
commonMain {
dependencies {}
dependencies {
implementation("co.touchlab:stately-concurrency:$statelyVersion")
implementation("co.touchlab:stately-isolate:$statelyIsoVersion")
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
}
}
}
}

View File

@ -0,0 +1,152 @@
package com.shabinder.common.caching
import kotlin.time.Duration
import kotlin.time.TimeSource
/**
* An in-memory key-value cache with support for time-based (expiration) and size-based evictions.
*/
public interface Cache<in Key : Any, Value : Any> {
/**
* Returns the value associated with [key] in this cache, or null if there is no
* cached value for [key].
*/
public fun get(key: Key): Value?
/**
* Returns the value associated with [key] in this cache if exists,
* otherwise gets the value by invoking [loader], associates the value with [key] in the cache,
* and returns the cached value.
*
* Any exceptions thrown by the [loader] will be propagated to the caller of this function.
*/
public suspend fun get(key: Key, loader: suspend () -> Value): Value
/**
* Associates [value] with [key] in this cache. If the cache previously contained a
* value associated with [key], the old value is replaced by [value].
*/
public fun put(key: Key, value: Value)
/**
* Discards any cached value for key [key].
*/
public fun invalidate(key: Key)
/**
* Discards all entries in the cache.
*/
public fun invalidateAll()
/**
* Returns a defensive copy of cache entries as [Map].
*/
public fun asMap(): Map<in Key, Value>
/**
* Main entry point for creating a [Cache].
*/
public interface Builder {
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation or the most recent replacement of its value.
*
* When [duration] is zero, the cache's max size will be set to 0
* meaning no values will be cached.
*/
public fun expireAfterWrite(duration: Duration): Builder
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
* access.
*
* When [duration] is zero, the cache's max size will be set to 0
* meaning no values will be cached.
*/
public fun expireAfterAccess(duration: Duration): Builder
/**
* Specifies the maximum number of entries the cache may contain.
* Cache eviction policy is based on LRU - i.e. least recently accessed entries get evicted first.
*
* When [size] is 0, entries will be discarded immediately and no values will be cached.
*
* If not set, cache size will be unlimited.
*/
public fun maximumCacheSize(size: Long): Builder
/**
* Specifies a [FakeTimeSource] for programmatically advancing the reading of the underlying
* [TimeSource] used for expiry checks in tests.
*
* If not specified, [TimeSource.Monotonic] will be used for expiry checks.
*/
public fun fakeTimeSource(fakeTimeSource: FakeTimeSource): Builder
/**
* Builds a new instance of [Cache] with the specified configurations.
*/
public fun <K : Any, V : Any> build(): Cache<K, V>
public companion object {
/**
* Returns a new [Cache.Builder] instance.
*/
public fun newBuilder(): Builder = CacheBuilderImpl()
}
}
}
/**
* A default implementation of [Cache.Builder].
*/
internal class CacheBuilderImpl : Cache.Builder {
private var expireAfterWriteDuration = Duration.INFINITE
private var expireAfterAccessDuration = Duration.INFINITE
private var maxSize = UNSET_LONG
private var fakeTimeSource: FakeTimeSource? = null
override fun expireAfterWrite(duration: Duration): CacheBuilderImpl = apply {
require(duration.isPositive()) {
"expireAfterWrite duration must be positive"
}
this.expireAfterWriteDuration = duration
}
override fun expireAfterAccess(duration: Duration): CacheBuilderImpl = apply {
require(duration.isPositive()) {
"expireAfterAccess duration must be positive"
}
this.expireAfterAccessDuration = duration
}
override fun maximumCacheSize(size: Long): CacheBuilderImpl = apply {
require(size >= 0) {
"maximum size must not be negative"
}
this.maxSize = size
}
override fun fakeTimeSource(fakeTimeSource: FakeTimeSource): CacheBuilderImpl = apply {
this.fakeTimeSource = fakeTimeSource
}
override fun <K : Any, V : Any> build(): Cache<K, V> {
return RealCache(
expireAfterWriteDuration,
expireAfterAccessDuration,
maxSize,
fakeTimeSource ?: TimeSource.Monotonic,
)
}
companion object {
internal const val UNSET_LONG: Long = -1
}
}

View File

@ -0,0 +1,51 @@
package com.shabinder.common.caching
import co.touchlab.stately.concurrency.AtomicLong
import kotlin.time.AbstractLongTimeSource
import kotlin.time.Duration
import kotlin.time.DurationUnit
/**
* A time source that has programmatically updatable readings with support for multi-threaded access in Kotlin/Native.
*
* Implementation is identical to [kotlin.time.TestTimeSource] except the internal [reading] is an [AtomicLong].
*/
public class FakeTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSECONDS) {
private val reading = AtomicLong(0)
override fun read(): Long = reading.get()
/**
* Advances the current reading value of this time source by the specified [duration].
*
* [duration] value is rounded down towards zero when converting it to a [Long] number of nanoseconds.
* For example, if the duration being added is `0.6.nanoseconds`, the reading doesn't advance because
* the duration value is rounded to zero nanoseconds.
*
* @throws IllegalStateException when the reading value overflows as the result of this operation.
*/
public operator fun plusAssign(duration: Duration) {
val delta = duration.toDouble(unit)
val longDelta = delta.toLong()
reading.set(
reading.get().let { currentReading ->
if (longDelta != Long.MIN_VALUE && longDelta != Long.MAX_VALUE) {
// when delta fits in long, add it as long
val newReading = currentReading + longDelta
if (currentReading xor longDelta >= 0 && currentReading xor newReading < 0) overflow(duration)
newReading
} else {
// when delta is greater than long, add it as double
val newReading = currentReading + delta
if (newReading > Long.MAX_VALUE || newReading < Long.MIN_VALUE) overflow(duration)
newReading.toLong()
}
}
)
}
private fun overflow(duration: Duration) {
throw IllegalStateException("FakeTimeSource will overflow if its reading ${reading}ns is advanced by $duration.")
}
}

View File

@ -0,0 +1,250 @@
package com.shabinder.common.caching
import co.touchlab.stately.collections.IsoMutableMap
import co.touchlab.stately.collections.IsoMutableSet
import co.touchlab.stately.concurrency.AtomicReference
import co.touchlab.stately.concurrency.value
import kotlin.time.Duration
import kotlin.time.TimeMark
import kotlin.time.TimeSource
/**
* A Kotlin Multiplatform [Cache] implementation powered by touchlab/Stately.
*
* Two types of evictions are supported:
*
* 1. Time-based evictions (expiration)
* 2. Size-based evictions
*
* Time-based evictions are enabled by specifying [expireAfterWriteDuration] and/or [expireAfterAccessDuration].
* When [expireAfterWriteDuration] is specified, entries will be automatically removed from the cache
* once a fixed duration has elapsed after the entry's creation
* or most recent replacement of its value.
* When [expireAfterAccessDuration] is specified, entries will be automatically removed from the cache
* once a fixed duration has elapsed after the entry's creation,
* the most recent replacement of its value, or its last access.
*
* Note that creation and replacement of an entry is also considered an access.
*
* Size-based evictions are enabled by specifying [maxSize]. When the size of the cache entries grows
* beyond [maxSize], least recently accessed entries will be evicted.
*/
internal class RealCache<Key : Any, Value : Any>(
val expireAfterWriteDuration: Duration,
val expireAfterAccessDuration: Duration,
val maxSize: Long,
val timeSource: TimeSource,
) : Cache<Key, Value> {
private val cacheEntries = IsoMutableMap<Key, CacheEntry<Key, Value>>()
/**
* Whether to perform size based evictions.
*/
private val evictsBySize = maxSize >= 0
/**
* Whether to perform write-time based expiration.
*/
private val expiresAfterWrite = expireAfterWriteDuration.isFinite()
/**
* Whether to perform access-time (both read and write) based expiration.
*/
private val expiresAfterAccess = expireAfterAccessDuration.isFinite()
/**
* A queue of unique cache entries ordered by write time.
* Used for performing write-time based cache expiration.
*/
private val writeQueue: IsoMutableSet<CacheEntry<Key, Value>>? =
takeIf { expiresAfterWrite }?.let {
ReorderingIsoMutableSet()
}
/**
* A queue of unique cache entries ordered by access time.
* Used for performing both write-time and read-time based cache expiration
* as well as size-based eviction.
*
* Note that a write is also considered an access.
*/
private val accessQueue: IsoMutableSet<CacheEntry<Key, Value>>? =
takeIf { expiresAfterAccess || evictsBySize }?.let {
ReorderingIsoMutableSet()
}
override fun get(key: Key): Value? {
return cacheEntries[key]?.let {
if (it.isExpired()) {
// clean up expired entries and return null
expireEntries()
null
} else {
// update eviction metadata
recordRead(it)
it.value.get()
}
}
}
override suspend fun get(key: Key, loader: suspend () -> Value): Value {
return cacheEntries[key]?.let {
if (it.isExpired()) {
// clean up expired entries
expireEntries()
null
} else {
// update eviction metadata
recordRead(it)
it.value.get()
}
} ?: loader().let { loadedValue ->
val existingValue = get(key)
if (existingValue != null) {
existingValue
} else {
put(key, loadedValue)
loadedValue
}
}
}
override fun put(key: Key, value: Value) {
expireEntries()
val existingEntry = cacheEntries[key]
if (existingEntry != null) {
// cache entry found
recordWrite(existingEntry)
existingEntry.value.set(value)
} else {
// create a new cache entry
val nowTimeMark = timeSource.markNow()
val newEntry = CacheEntry(
key = key,
value = AtomicReference(value),
accessTimeMark = AtomicReference(nowTimeMark),
writeTimeMark = AtomicReference(nowTimeMark),
)
recordWrite(newEntry)
cacheEntries[key] = newEntry
}
evictEntries()
}
override fun invalidate(key: Key) {
expireEntries()
cacheEntries.remove(key)?.also {
writeQueue?.remove(it)
accessQueue?.remove(it)
}
}
override fun invalidateAll() {
cacheEntries.clear()
writeQueue?.clear()
accessQueue?.clear()
}
override fun asMap(): Map<in Key, Value> {
return cacheEntries.values.associate { entry ->
entry.key to entry.value.get()
}
}
/**
* Remove all expired entries.
*/
private fun expireEntries() {
val queuesToProcess = listOfNotNull(
if (expiresAfterWrite) writeQueue else null,
if (expiresAfterAccess) accessQueue else null
)
queuesToProcess.forEach { queue ->
queue.access {
val iterator = queue.iterator()
for (entry in iterator) {
if (entry.isExpired()) {
cacheEntries.remove(entry.key)
// remove the entry from the current queue
iterator.remove()
} else {
// found unexpired entry, no need to look any further
break
}
}
}
}
}
/**
* Check whether the [CacheEntry] has expired based on either access time or write time.
*/
private fun CacheEntry<Key, Value>.isExpired(): Boolean {
return expiresAfterAccess && (accessTimeMark.get() + expireAfterAccessDuration).hasPassedNow() ||
expiresAfterWrite && (writeTimeMark.get() + expireAfterWriteDuration).hasPassedNow()
}
/**
* Evict least recently accessed entries until [cacheEntries] is no longer over capacity.
*/
private fun evictEntries() {
if (!evictsBySize) {
return
}
checkNotNull(accessQueue)
while (cacheEntries.size > maxSize) {
accessQueue.access {
it.firstOrNull()?.run {
cacheEntries.remove(key)
writeQueue?.remove(this)
accessQueue.remove(this)
}
}
}
}
/**
* Update the eviction metadata on the [cacheEntry] which has just been read.
*/
private fun recordRead(cacheEntry: CacheEntry<Key, Value>) {
if (expiresAfterAccess) {
val accessTimeMark = cacheEntry.accessTimeMark.value
cacheEntry.accessTimeMark.set(accessTimeMark + accessTimeMark.elapsedNow())
}
accessQueue?.add(cacheEntry)
}
/**
* Update the eviction metadata on the [CacheEntry] which is about to be written.
* Note that a write is also considered an access.
*/
private fun recordWrite(cacheEntry: CacheEntry<Key, Value>) {
if (expiresAfterAccess) {
val accessTimeMark = cacheEntry.accessTimeMark.value
cacheEntry.accessTimeMark.set(accessTimeMark + accessTimeMark.elapsedNow())
}
if (expiresAfterWrite) {
val writeTimeMark = cacheEntry.writeTimeMark.value
cacheEntry.writeTimeMark.set(writeTimeMark + writeTimeMark.elapsedNow())
}
accessQueue?.add(cacheEntry)
writeQueue?.add(cacheEntry)
}
}
/**
* A cache entry holds the [key] and [value] pair,
* along with the metadata needed to perform cache expiration and eviction.
*/
private class CacheEntry<Key : Any, Value : Any>(
val key: Key,
val value: AtomicReference<Value>,
val accessTimeMark: AtomicReference<TimeMark>,
val writeTimeMark: AtomicReference<TimeMark>,
)

View File

@ -0,0 +1,17 @@
package com.shabinder.common.caching
import co.touchlab.stately.collections.IsoMutableSet
/**
* A custom [IsoMutableSet] that updates the insertion order when an element is re-inserted,
* i.e. an inserted element will always be placed at the end
* regardless of whether the element already exists.
*/
internal class ReorderingIsoMutableSet<T> : IsoMutableSet<T>(), MutableSet<T> {
override fun add(element: T): Boolean = access {
val exists = remove(element)
super.add(element)
// respect the contract "true if this set did not already contain the specified element"
!exists
}
}

View File

@ -42,8 +42,8 @@ interface Actions {
}
private fun stubActions() = object :Actions{
override val platformActions = object: PlatformActions{}
private fun stubActions() = object :Actions {
override val platformActions = StubPlatformActions
override fun showPopUpMessage(string: String, long: Boolean) {}
override fun setDownloadDirectoryAction() {}
override fun queryActiveTracks() {}

View File

@ -21,6 +21,6 @@
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:fillColor="#FFFFFF"
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="456"
android:viewportWidth="456" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M61.179,282h-41.2c-6,0 -10.9,4.9 -10.9,10.9v152.2c0,6 4.9,10.9 10.9,10.9h41.2c6,0 10.9,-4.9 10.9,-10.9V292.9C72.079,286.9 67.179,282 61.179,282z"/>
<path android:fillColor="#FFFFFF" android:pathData="M443.179,294.4c-0.3,-0.4 -0.6,-0.8 -0.8,-1.2c-6.1,-6.8 -16.4,-7.7 -23.6,-2.1c-20,16.3 -49.2,39.8 -68.1,55.1c-16.7,13.3 -37.3,21 -58.7,21.8l-51.2,1.7c-9.4,0.3 -18.2,-4.5 -23,-12.6l-5.7,-9.7c-1.5,-2.5 -2.5,-5.3 -3.1,-8.2c-2.6,-13.9 6.5,-27.4 20.4,-30l52.9,-10c8.5,-1.7 14.3,-9.7 13.3,-18.3c-1,-8.2 -8,-14.3 -16.2,-14.4c-0.3,0 -0.7,0 -0.8,0c-0.4,0 -0.9,0 -1.3,0l-71.2,-2.9c-24.7,-0.9 -46,3.9 -71.2,16.1l-42.8,20.7v114l35.9,-6.6c0.1,-0.1 0.2,-0.1 0.3,-0.1c13.9,-2 23.1,-2.9 38.2,-2.2l107.5,5c33.7,1.4 66.8,-9.7 92.7,-31.3l74.4,-61.7C447.979,311.7 448.879,301.3 443.179,294.4z"/>
<path android:fillColor="#FFFFFF" android:pathData="M307.379,0c-61.2,0.1 -110.7,49.7 -110.8,110.8c0,0.1 0,0.1 0,0.1c0,61.2 49.7,110.8 110.9,110.8s110.8,-49.7 110.8,-110.9S368.579,0 307.379,0zM333.079,80h13.4c5.5,0 10,4.5 10,10s-4.5,10 -10,10h-13.4c-2,7.8 -6,14.9 -11.7,20.6c-7.5,7.5 -17.5,11.9 -28.1,12.5l37.7,35.7c4,3.8 4.2,10.1 0.4,14.1s-10.1,4.2 -14.1,0.4l-55.9,-52.9c-1.9,-1.9 -3.1,-4.5 -3.1,-7.2c-0.1,-5.6 4.4,-10.1 10,-10.2h22.4c6.2,0.1 12.2,-2.3 16.7,-6.7c1.8,-1.9 3.3,-4 4.6,-6.3h-43.7c-5.5,0 -10,-4.5 -10,-10s4.5,-10 10,-10h43.7c-3.7,-8 -11.9,-14 -21.3,-14h-22.4c-5.5,0 -10,-4.5 -10,-10s4.5,-10 10,-10h78.2c5.5,0 10,4.5 10,10s-4.5,10 -10,10h-19.2C330.079,70.3 331.979,75 333.079,80z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:viewportHeight="435.505"
android:viewportWidth="435.505" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M403.496,101.917c-4.104,-5.073 -8.877,-9.705 -14.166,-13.839c0.707,13.117 -0.508,27.092 -3.668,41.884c-8.627,40.413 -29.256,74.754 -59.656,99.304c-30.375,24.533 -68.305,37.502 -109.686,37.502h-60.344l-19.533,91.512c-3.836,17.959 -19.943,30.99 -38.303,30.99H70.938l-4.898,22.484c-1.258,5.79 0.17,11.839 3.887,16.453c3.715,4.614 9.324,7.298 15.25,7.298h66.498c9.24,0 17.225,-6.459 19.152,-15.495L193.667,313h76.188c36.854,0 70.527,-11.464 97.384,-33.152c26.869,-21.697 45.129,-52.186 52.807,-88.162C427.822,155.309 422.253,125.106 403.496,101.917z"/>
<path android:fillColor="#FFFFFF" android:pathData="M117.292,354.191l22.84,-107.008h76.188c36.852,0 70.527,-11.465 97.383,-33.154c26.867,-21.697 45.129,-52.186 52.809,-88.161c7.773,-36.378 2.207,-66.58 -16.553,-89.769C331.952,13.832 301.17,0 269.633,0H103.639c-9.209,0 -17.174,6.417 -19.135,15.414L12.505,345.938c-1.26,5.789 0.168,11.838 3.887,16.453c3.713,4.613 9.32,7.296 15.248,7.296h66.5C107.38,369.687 115.36,363.229 117.292,354.191zM178.235,75.291h52.229c12.287,0 23.274,5.149 30.145,14.129c7.297,9.539 9.431,22.729 5.853,36.188c-0.047,0.171 -0.088,0.342 -0.131,0.516c-6.57,27.73 -33.892,50.291 -60.898,50.291h-50.05L178.235,75.291z"/>
</vector>

View File

@ -41,7 +41,6 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation(Extras.Android.razorpay)
implementation(Extras.mp3agic)
//implementation(Extras.jaudioTagger)
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
// implementation(files("$rootDir/libs/mobile-ffmpeg.aar"))
}

View File

@ -28,6 +28,7 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.list.store.SpotiFlyerListStoreProvider
import com.shabinder.common.list.store.getStore
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.caching.Cache
internal class SpotiFlyerListImpl(
componentContext: ComponentContext,
@ -49,6 +50,11 @@ internal class SpotiFlyerListImpl(
).provide()
}
private val cache = Cache.Builder
.newBuilder()
.maximumCacheSize(150)
.build<String, Picture>()
override val models: Value<State> = store.asValue()
override fun onDownloadAllClicked(trackList: List<TrackDetails>) {
@ -67,5 +73,9 @@ internal class SpotiFlyerListImpl(
store.accept(Intent.RefreshTracksStatuses)
}
override suspend fun loadImage(url: String): Picture = dir.loadImage(url)
override suspend fun loadImage(url: String): Picture {
return cache.get(url) {
dir.loadImage(url)
}
}
}

View File

@ -44,6 +44,7 @@ internal class SpotiFlyerListStoreProvider(
private val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
) {
val logger = getLogger()
fun provide(): SpotiFlyerListStore =
object :
SpotiFlyerListStore,

View File

@ -18,6 +18,7 @@ package com.shabinder.common.main.integration
import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.lifecycle.doOnDestroy
import com.arkivanov.decompose.value.Value
import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue
@ -30,6 +31,7 @@ import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
import com.shabinder.common.main.store.getStore
import com.shabinder.common.models.methods
import com.shabinder.common.caching.Cache
internal class SpotiFlyerMainImpl(
componentContext: ComponentContext,
@ -38,6 +40,9 @@ internal class SpotiFlyerMainImpl(
init {
instanceKeeper.ensureNeverFrozen()
lifecycle.doOnDestroy {
cache.invalidateAll()
}
}
private val store =
@ -48,6 +53,11 @@ internal class SpotiFlyerMainImpl(
).provide()
}
private val cache = Cache.Builder
.newBuilder()
.maximumCacheSize(20)
.build<String, Picture>()
override val models: Value<State> = store.asValue()
override fun onLinkSearch(link: String) {
@ -63,5 +73,9 @@ internal class SpotiFlyerMainImpl(
store.accept(Intent.SelectCategory(category))
}
override suspend fun loadImage(url: String): Picture = dir.loadImage(url)
override suspend fun loadImage(url: String): Picture {
return cache.get(url) {
dir.loadImage(url)
}
}
}

@ -1 +1 @@
Subproject commit 777e45a555bcb7a8a0713705cbf159d05a45adb0
Subproject commit 30c16869ebc81aace8a21cc0ecb0f0314f72b0bc

View File

@ -16,14 +16,12 @@
plugins {
kotlin("js")
//id("org.jetbrains.kotlin.js") version "1.4.31"
}
group = "com.shabinder"
version = "0.1"
repositories {
jcenter()
mavenCentral()
maven(url = "https://dl.bintray.com/kotlin/kotlin-js-wrappers")
}