Optimize extensions and components and packages

This commit is contained in:
Ash 2022-04-03 00:30:25 +08:00
parent 257a6332ac
commit 622d0e2cfd
78 changed files with 292 additions and 942 deletions

View File

@ -14,6 +14,9 @@ import me.ash.reader.data.repository.*
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
import javax.inject.Inject
@HiltAndroidApp

View File

@ -1,3 +0,0 @@
package me.ash.reader
fun Int.spacerDollar(str: Any): String = "$this$$str"

View File

@ -1,17 +0,0 @@
package me.ash.reader.data
import androidx.room.TypeConverter
import java.util.*
class Converters {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
return dateLong?.let { Date(it) }
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
}

View File

@ -1,6 +1,7 @@
package me.ash.reader.data.account
package me.ash.reader.data.dao
import androidx.room.*
import me.ash.reader.data.entity.Account
@Dao
interface AccountDao {

View File

@ -1,8 +1,11 @@
package me.ash.reader.data.article
package me.ash.reader.data.dao
import androidx.paging.PagingSource
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.ImportantCount
@Dao
interface ArticleDao {

View File

@ -1,6 +1,7 @@
package me.ash.reader.data.feed
package me.ash.reader.data.dao
import androidx.room.*
import me.ash.reader.data.entity.Feed
@Dao
interface FeedDao {

View File

@ -1,7 +1,9 @@
package me.ash.reader.data.group
package me.ash.reader.data.dao
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Group
import me.ash.reader.data.entity.GroupWithFeed
@Dao
interface GroupDao {

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.account
package me.ash.reader.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity

View File

@ -1,10 +1,9 @@
package me.ash.reader.data.article
package me.ash.reader.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import me.ash.reader.data.feed.Feed
import java.util.*
@Entity(

View File

@ -1,8 +1,7 @@
package me.ash.reader.data.article
package me.ash.reader.data.entity
import androidx.room.Embedded
import androidx.room.Relation
import me.ash.reader.data.feed.Feed
data class ArticleWithFeed(
@Embedded

View File

@ -1,7 +1,6 @@
package me.ash.reader.data.feed
package me.ash.reader.data.entity
import androidx.room.*
import me.ash.reader.data.group.Group
@Entity(
tableName = "feed",

View File

@ -1,8 +1,7 @@
package me.ash.reader.data.feed
package me.ash.reader.data.entity
import androidx.room.Embedded
import androidx.room.Relation
import me.ash.reader.data.article.Article
data class FeedWithArticle(
@Embedded

View File

@ -1,8 +1,7 @@
package me.ash.reader.data.feed
package me.ash.reader.data.entity
import androidx.room.Embedded
import androidx.room.Relation
import me.ash.reader.data.group.Group
data class FeedWithGroup(
@Embedded

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.constant
package me.ash.reader.data.entity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.group
package me.ash.reader.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity

View File

@ -1,8 +1,7 @@
package me.ash.reader.data.group
package me.ash.reader.data.entity
import androidx.room.Embedded
import androidx.room.Relation
import me.ash.reader.data.feed.Feed
data class GroupWithFeed(
@Embedded

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.article
package me.ash.reader.data.entity
data class ImportantCount(
val important: Int,

View File

@ -6,10 +6,10 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.source.ReaderDatabase
import javax.inject.Singleton

View File

@ -10,18 +10,13 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.data.article.ImportantCount
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.*
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor(

View File

@ -3,12 +3,12 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.Account
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.spacerDollar
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import javax.inject.Inject
class AccountRepository @Inject constructor(

View File

@ -8,20 +8,20 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.spacerDollar
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import net.dankito.readability4j.extended.Readability4JExtended
import java.util.*
import javax.inject.Inject

View File

@ -14,18 +14,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.*

View File

@ -8,13 +8,13 @@ import be.ceau.opml.entity.Opml
import be.ceau.opml.entity.Outline
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.spacerDollar
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import java.io.InputStream
import java.util.*
import javax.inject.Inject

View File

@ -6,14 +6,14 @@ import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.currentAccountId
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.feed.FeedWithArticle
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.FeedWithArticle
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.spacerDollar
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import net.dankito.readability4j.Readability4J
import net.dankito.readability4j.extended.Readability4JExtended
import okhttp3.OkHttpClient

View File

@ -2,8 +2,8 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.currentAccountType
import me.ash.reader.data.account.Account
import me.ash.reader.data.entity.Account
import me.ash.reader.ui.ext.currentAccountType
import javax.inject.Inject
class RssRepository @Inject constructor(

View File

@ -5,11 +5,11 @@ import be.ceau.opml.OpmlParser
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.currentAccountId
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.ui.ext.currentAccountId
import java.io.InputStream
import java.util.*
import javax.inject.Inject

View File

@ -1,26 +1,23 @@
package me.ash.reader.data.source
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import me.ash.reader.data.Converters
import me.ash.reader.data.account.Account
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import androidx.room.*
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import java.util.*
@Database(
entities = [Account::class, Feed::class, Article::class, Group::class],
version = 1,
exportSchema = false,
)
@TypeConverters(Converters::class)
@TypeConverters(ReaderDatabase.Converters::class)
abstract class ReaderDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao
abstract fun feedDao(): FeedDao
@ -42,4 +39,17 @@ abstract class ReaderDatabase : RoomDatabase() {
}
}
}
class Converters {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
return dateLong?.let { Date(it) }
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
}
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutSlowInEasing

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.DropdownMenu

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -153,7 +153,7 @@ fun SelectionEditorChip(
onValueChange = { onValueChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
textStyle = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
decorationBox = { innerTextField ->
Row(

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.fillMaxWidth

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.component
import android.content.Intent
import android.graphics.Bitmap
@ -8,6 +8,9 @@ import android.util.Log
import android.webkit.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
@ -29,80 +32,94 @@ fun WebView(
val context = LocalContext.current
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
val webViewClient = object : WebViewClient() {
val webViewClient by remember {
mutableStateOf(object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
if (url != null && url.contains(INJECTION_TOKEN)) {
try {
val assetPath = url.substring(
url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length,
url.length
)
return WebResourceResponse(
"text/HTML",
"UTF-8",
context.assets.open(assetPath)
)
} catch (e: Exception) {
Log.e("RLog", "WebView shouldInterceptRequest: $e")
override fun shouldInterceptRequest(
view: WebView?,
url: String?
): WebResourceResponse? {
if (url != null && url.contains(INJECTION_TOKEN)) {
try {
val assetPath = url.substring(
url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length,
url.length
)
return WebResourceResponse(
"text/HTML",
"UTF-8",
context.assets.open(assetPath)
)
} catch (e: Exception) {
Log.e("RLog", "WebView shouldInterceptRequest: $e")
}
}
return super.shouldInterceptRequest(view, url);
}
return super.shouldInterceptRequest(view, url);
}
override fun onPageStarted(
view: WebView?,
url: String?,
favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
override fun onPageStarted(
view: WebView?,
url: String?,
favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
// _isLoading = true
onProgressChange(-1)
}
onProgressChange(-1)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val jsCode = "javascript:(function(){" +
"var imgs=document.getElementsByTagName(\"img\");" +
"for(var i=0;i<imgs.length;i++){" +
"imgs[i].pos = i;" +
"imgs[i].onclick=function(){" +
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val jsCode = "javascript:(function(){" +
"var imgs=document.getElementsByTagName(\"img\");" +
"for(var i=0;i<imgs.length;i++){" +
"imgs[i].pos = i;" +
"imgs[i].onclick=function(){" +
// "window.jsCallJavaObj.openImage(this.src,this.pos);" +
"alert('asf');" +
"}}})()"
view!!.loadUrl(jsCode)
viewModel.dispatch(ReadViewAction.ChangeLoading(false))
onProgressChange(100)
}
"alert('asf');" +
"}}})()"
view!!.loadUrl(jsCode)
viewModel.dispatch(ReadViewAction.ChangeLoading(false))
onProgressChange(100)
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (null == request?.url) return false
val url = request.url.toString()
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(url))
)
return true
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (null == request?.url) return false
val url = request.url.toString()
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(url))
)
return true
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
onReceivedError(error)
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
onReceivedError(error)
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.cancel()
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.cancel()
}
})
}
val webView by remember {
mutableStateOf(WebView(context).apply {
this.webViewClient = webViewClient
setBackgroundColor(backgroundColor)
isHorizontalScrollBarEnabled = false
isVerticalScrollBarEnabled = false
})
}
// Column(
@ -121,14 +138,7 @@ fun WebView(
AndroidView(
modifier = modifier,
factory = {
WebView(it).apply {
this.webViewClient = webViewClient
setBackgroundColor(backgroundColor)
isHorizontalScrollBarEnabled = false
isVerticalScrollBarEnabled = false
}
},
factory = { webView },
update = {
it.apply {
Log.i("RLog", "CustomWebView: ${content}")

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import android.app.Activity
import android.content.Context

View File

@ -1,4 +1,4 @@
package me.ash.reader
package me.ash.reader.ui.ext
import android.content.Context
import android.util.Log

View File

@ -1,12 +1,13 @@
package me.ash.reader
package me.ash.reader.ui.ext
import android.content.Context
import androidx.core.os.ConfigurationCompat
import me.ash.reader.R
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
fun Date.formatToString(
fun Date.formatAsString(
context: Context,
onlyHourMinute: Boolean? = false,
atHourMinute: Boolean? = false,

View File

@ -1,9 +1,9 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.entity.Filter
@Composable
fun Filter.getName(): String = when (this) {

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import androidx.compose.foundation.lazy.LazyListState
import kotlin.math.abs

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding

View File

@ -0,0 +1,3 @@
package me.ash.reader.ui.ext
fun Int.spacerDollar(str: Any): String = "$this$$str"

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.extension
package me.ash.reader.ui.ext
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState

View File

@ -0,0 +1,12 @@
package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
@Composable
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = Dispatchers.Default
): T = collectAsState(context).value

View File

@ -1,4 +1,4 @@
package me.ash.reader
package me.ash.reader.ui.ext
fun String.formatUrl(): String {
if (this.startsWith("//")) {

View File

@ -1,23 +0,0 @@
package me.ash.reader.ui.extension
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KProperty
@Composable
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = Dispatchers.Default
): T = collectAsState(context).value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

View File

@ -7,8 +7,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.data.constant.Filter
import me.ash.reader.ui.extension.getName
import me.ash.reader.data.entity.Filter
import me.ash.reader.ui.ext.getName
@OptIn(ExperimentalPagerApi::class)
@Composable
@ -21,7 +21,10 @@ fun FilterBar(
modifier = Modifier.height(60.dp)
) {
Divider(
modifier = Modifier.fillMaxWidth().height(1.dp).zIndex(1f),
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
NavigationBar(

View File

@ -10,8 +10,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.launch
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.component.ViewPager
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
@ -21,7 +22,6 @@ import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.widget.ViewPager
@OptIn(ExperimentalPagerApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable

View File

@ -9,12 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.Group
import me.ash.reader.data.repository.AbstractRssRepository
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.extension.animateScrollToPage
import me.ash.reader.ui.ext.animateScrollToPage
import javax.inject.Inject
@OptIn(ExperimentalPagerApi::class)

View File

@ -14,8 +14,8 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog
import me.ash.reader.ui.component.Dialog
import me.ash.reader.ui.ext.collectAsStateValue
@OptIn(ExperimentalPagerApi::class)
@Composable

View File

@ -17,11 +17,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.component.BottomDrawer
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage
import me.ash.reader.ui.widget.BottomDrawer
import me.ash.reader.ui.widget.Subtitle
@OptIn(ExperimentalMaterialApi::class)
@Composable

View File

@ -15,8 +15,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject

View File

@ -18,7 +18,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel

View File

@ -28,16 +28,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.repository.AbstractRssRepository
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getDesc
import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getDesc
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.widget.Banner
import me.ash.reader.ui.widget.Subtitle
@OptIn(
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,

View File

@ -8,9 +8,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.account.Account
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.repository.AccountRepository
import me.ash.reader.data.repository.OpmlRepository
import me.ash.reader.data.repository.RssRepository

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.entity.Feed
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
@ -57,7 +57,9 @@ fun GroupItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f).padding(start = 28.dp),
modifier = Modifier
.weight(1f)
.padding(start = 28.dp),
text = text,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,

View File

@ -22,11 +22,11 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R
import me.ash.reader.data.group.Group
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.SelectionChip
import me.ash.reader.ui.widget.SelectionEditorChip
import me.ash.reader.ui.widget.Subtitle
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.SelectionChip
import me.ash.reader.ui.component.SelectionEditorChip
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.roundClick
@Composable
fun ResultViewPage(

View File

@ -21,10 +21,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.*
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog
import me.ash.reader.ui.component.Dialog
import me.ash.reader.ui.ext.*
@OptIn(ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable

View File

@ -9,15 +9,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import me.ash.reader.R
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.repository.OpmlRepository
import me.ash.reader.data.repository.RssHelper
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.formatUrl
import me.ash.reader.ui.extension.animateScrollToPage
import me.ash.reader.ui.ext.animateScrollToPage
import me.ash.reader.ui.ext.formatUrl
import java.io.InputStream
import javax.inject.Inject

View File

@ -6,8 +6,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.data.group.Group
import me.ash.reader.ui.widget.ViewPager
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.ViewPager
@OptIn(ExperimentalPagerApi::class)
@Composable

View File

@ -15,8 +15,8 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString
@Composable
fun ArticleItem(
@ -40,7 +40,9 @@ fun ArticleItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f).padding(start = 30.dp),
modifier = Modifier
.weight(1f)
.padding(start = 30.dp),
text = articleWithFeed.feed.name,
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.labelMedium,
@ -49,7 +51,7 @@ fun ArticleItem(
)
Text(
modifier = Modifier.padding(start = 6.dp),
text = articleWithFeed.article.date.formatToString(context, onlyHourMinute = true),
text = articleWithFeed.article.date.formatAsString(context, onlyHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)

View File

@ -8,8 +8,8 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.generateArticleList(
@ -20,7 +20,7 @@ fun LazyListScope.generateArticleList(
var lastItemDay: String? = null
for (itemIndex in 0 until pagingItems.itemCount) {
val currentItem = pagingItems.peek(itemIndex) ?: continue
val currentItemDay = currentItem.article.date.formatToString(context)
val currentItemDay = currentItem.article.date.formatAsString(context)
if (lastItemDay != currentItemDay) {
if (itemIndex != 0) {
item { Spacer(modifier = Modifier.height(40.dp)) }

View File

@ -24,9 +24,9 @@ import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getName
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState

View File

@ -11,7 +11,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.page.home.FilterState
import javax.inject.Inject

View File

@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString
import me.ash.reader.ui.ext.roundClick
@Composable
fun Header(
@ -30,7 +30,7 @@ fun Header(
.padding(12.dp)
) {
Text(
text = articleWithFeed.article.date.formatToString(context, atHourMinute = true),
text = articleWithFeed.article.date.formatAsString(context, atHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.ui.widget.CanBeDisabledIconButton
import me.ash.reader.ui.component.CanBeDisabledIconButton
@Composable
fun ReadBar(

View File

@ -21,9 +21,9 @@ import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.WebView
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.component.WebView
import me.ash.reader.ui.ext.collectAsStateValue
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssHelper
import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject

View File

@ -11,7 +11,6 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -20,9 +19,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.TopTitleBox
import me.ash.reader.ui.ext.paddingFixedHorizontal
import me.ash.reader.ui.ext.roundClick
@Composable
fun SettingsPage(
@ -33,17 +31,6 @@ fun SettingsPage(
// LargeTopAppBar(
// title = { Text(text = "Settings") }
// )
TopTitleBox(
title = "Settings",
description = "",
listState = listState,
startOffset = Offset(20f, 78f),
startHeight = 72f,
startTitleFontSize = 36f,
startDescriptionFontSize = 0f,
) {
}
Column {
SmallTopAppBar(
title = {},

View File

@ -1,41 +0,0 @@
package me.ash.reader.ui.widget
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@Composable
fun AnimateLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
reference: Any?,
content: LazyListScope.() -> Unit,
) {
var visible by remember { mutableStateOf(true) }
LaunchedEffect(reference) {
Log.i("RLog", "reference change")
visible = false
// delay(50)
visible = true
}
AnimatedVisibility(
modifier = modifier.fillMaxSize(),
visible = visible,
enter = fadeIn() + expandVertically(),
) {
LazyColumn(
modifier = modifier,
state = state,
content = content,
)
}
}

View File

@ -1,134 +0,0 @@
import android.annotation.SuppressLint
import androidx.compose.animation.*
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.*
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/**
* @param state Use [updateAnimatedItemsState].
*/
inline fun <T> LazyListScope.animatedItemsIndexed(
state: List<AnimatedItem<T>>,
enterTransition: EnterTransition = expandVertically(),
exitTransition: ExitTransition = shrinkVertically(),
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
items(
state.size,
if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
) { index ->
val item = state[index]
val visibility = item.visibility
key(key?.invoke(item.item)) {
AnimatedVisibility(
visibleState = visibility,
enter = enterTransition,
exit = exitTransition
) {
itemContent(index, item.item)
}
}
}
}
@Composable
fun <T> updateAnimatedItemsState(
newList: List<T>
): State<List<AnimatedItem<T>>> {
val state = remember { mutableStateOf(emptyList<AnimatedItem<T>>()) }
LaunchedEffect(newList) {
if (state.value == newList) {
return@LaunchedEffect
}
val oldList = state.value.toList()
val diffCb = object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
}
val diffResult = calculateDiff(false, diffCb)
val compositeList = oldList.toMutableList()
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
for (i in 0 until count) {
val newItem = AnimatedItem(
visibility = MutableTransitionState(false),
newList[position + i]
)
newItem.visibility.targetState = true
compositeList.add(position + i, newItem)
}
}
override fun onRemoved(position: Int, count: Int) {
for (i in 0 until count) {
compositeList[position + i].visibility.targetState = false
}
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
// not detecting moves.
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
// irrelevant with compose.
}
})
if (state.value != compositeList) {
state.value = compositeList
}
val initialAnimation = Animatable(1.0f)
initialAnimation.animateTo(0f)
state.value = state.value.filter { it.visibility.targetState }
}
return state
}
data class AnimatedItem<T>(
val visibility: MutableTransitionState<Boolean>,
val item: T,
) {
override fun hashCode(): Int {
return item?.hashCode() ?: 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AnimatedItem<*>
if (item != other.item) return false
return true
}
}
suspend fun calculateDiff(
detectMoves: Boolean = true,
diffCb: DiffUtil.Callback
): DiffUtil.DiffResult {
return withContext(Dispatchers.Unconfined) {
DiffUtil.calculateDiff(diffCb, detectMoves)
}
}

View File

@ -1,305 +0,0 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.roundToInt
import kotlin.math.sign
//val items = listOf(
// Color.Red,
// Color.Blue,
// Color.Green,
// Color.Yellow,
// Color.Cyan,
// Color.Magenta,
//)
@Composable
fun <T : Any> CustomPager(
items: List<T>,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
initialIndex: Int = 0,
/*@FloatRange(from = 0.0, to = 1.0)*/
itemFraction: Float = 1f,
itemSpacing: Dp = 0.dp,
/*@FloatRange(from = 0.0, to = 1.0)*/
overshootFraction: Float = .5f,
onItemSelect: (T) -> Unit = {},
contentFactory: @Composable (T) -> Unit,
) {
Pager(
items,
modifier,
orientation,
initialIndex,
itemFraction,
itemSpacing,
overshootFraction,
onItemSelect = { index -> onItemSelect(items[index]) },
) {
items.forEach { item ->
Box(
modifier = when (orientation) {
Orientation.Horizontal -> Modifier.fillMaxWidth()
Orientation.Vertical -> Modifier.fillMaxHeight()
},
contentAlignment = Alignment.Center,
) {
contentFactory(item)
}
}
}
}
@Composable
fun <T : Any> Pager(
items: List<T>,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
initialIndex: Int = 0,
/*@FloatRange(from = 0.0, to = 1.0)*/
itemFraction: Float = 1f,
itemSpacing: Dp = 0.dp,
/*@FloatRange(from = 0.0, to = 1.0)*/
overshootFraction: Float = .5f,
onItemSelect: (Int) -> Unit = {},
content: @Composable () -> Unit,
) {
require(initialIndex in 0..items.lastIndex) { "Initial index out of bounds" }
require(itemFraction > 0f && itemFraction <= 1f) { "Item fraction must be in the (0f, 1f] range" }
require(overshootFraction > 0f && itemFraction <= 1f) { "Overshoot fraction must be in the (0f, 1f] range" }
val scope = rememberCoroutineScope()
val state = rememberPagerState()
state.currentIndex = initialIndex
state.numberOfItems = items.size
state.itemFraction = itemFraction
state.overshootFraction = overshootFraction
state.itemSpacing = with(LocalDensity.current) { itemSpacing.toPx() }
state.orientation = orientation
state.listener = onItemSelect
state.scope = scope
Layout(
content = content,
modifier = modifier
.clipToBounds()
.then(state.inputModifier),
) { measurables, constraints ->
val dimension = constraints.dimension(orientation)
val looseConstraints = constraints.toLooseConstraints(orientation, state.itemFraction)
val placeables = measurables.map { measurable -> measurable.measure(looseConstraints) }
val size = placeables.getSize(orientation, dimension)
val itemDimension = (dimension * state.itemFraction).roundToInt()
state.itemDimension = itemDimension
val halfItemDimension = itemDimension / 2
layout(size.width, size.height) {
val centerOffset = dimension / 2 - halfItemDimension
val dragOffset = state.dragOffset.value
val roundedDragOffset = dragOffset.roundToInt()
val spacing = state.itemSpacing.roundToInt()
val itemDimensionWithSpace = itemDimension + state.itemSpacing
val first = ceil(
(dragOffset - itemDimension - centerOffset) / itemDimensionWithSpace
).toInt().coerceAtLeast(0)
val last = ((dimension + dragOffset - centerOffset) / itemDimensionWithSpace).toInt()
.coerceAtMost(items.lastIndex)
for (i in first..last) {
val offset = i * (itemDimension + spacing) - roundedDragOffset + centerOffset
placeables[i].place(
x = when (orientation) {
Orientation.Horizontal -> offset
Orientation.Vertical -> 0
},
y = when (orientation) {
Orientation.Horizontal -> 0
Orientation.Vertical -> offset
}
)
}
}
}
LaunchedEffect(key1 = items, key2 = initialIndex) {
state.snapTo(initialIndex)
}
}
@Composable
private fun rememberPagerState(): PagerState = remember { PagerState() }
private fun Constraints.dimension(orientation: Orientation) = when (orientation) {
Orientation.Horizontal -> maxWidth
Orientation.Vertical -> maxHeight
}
private fun Constraints.toLooseConstraints(
orientation: Orientation,
itemFraction: Float,
): Constraints {
val dimension = dimension(orientation)
return when (orientation) {
Orientation.Horizontal -> copy(
minWidth = (dimension * itemFraction).roundToInt(),
maxWidth = (dimension * itemFraction).roundToInt(),
minHeight = 0,
)
Orientation.Vertical -> copy(
minWidth = 0,
minHeight = (dimension * itemFraction).roundToInt(),
maxHeight = (dimension * itemFraction).roundToInt(),
)
}
}
private fun List<Placeable>.getSize(
orientation: Orientation,
dimension: Int,
): IntSize {
return when (orientation) {
Orientation.Horizontal -> IntSize(
dimension,
maxByOrNull { it.height }?.height ?: 0
)
Orientation.Vertical -> IntSize(
maxByOrNull { it.width }?.width ?: 0,
dimension
)
}
}
private class PagerState {
var currentIndex by mutableStateOf(0)
var numberOfItems by mutableStateOf(0)
var itemFraction by mutableStateOf(0f)
var overshootFraction by mutableStateOf(0f)
var itemSpacing by mutableStateOf(0f)
var itemDimension by mutableStateOf(0)
var orientation by mutableStateOf(Orientation.Horizontal)
var scope: CoroutineScope? by mutableStateOf(null)
var listener: (Int) -> Unit by mutableStateOf({})
val dragOffset = Animatable(0f)
private val animationSpec = SpringSpec<Float>(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
suspend fun snapTo(index: Int) {
dragOffset.snapTo(index.toFloat() * (itemDimension + itemSpacing))
}
val inputModifier = Modifier.pointerInput(numberOfItems) {
fun itemIndex(offset: Int): Int = (offset / (itemDimension + itemSpacing)).roundToInt()
.coerceIn(0, numberOfItems - 1)
fun updateIndex(offset: Float) {
val index = itemIndex(offset.roundToInt())
if (index != currentIndex) {
currentIndex = index
listener(index)
}
}
fun calculateOffsetLimit(): OffsetLimit {
val dimension = when (orientation) {
Orientation.Horizontal -> size.width
Orientation.Vertical -> size.height
}
val itemSideMargin = (dimension - itemDimension) / 2f
return OffsetLimit(
min = -dimension * overshootFraction + itemSideMargin,
max = numberOfItems * (itemDimension + itemSpacing) - (1f - overshootFraction) * dimension + itemSideMargin,
)
}
forEachGesture {
awaitPointerEventScope {
val tracker = VelocityTracker()
val decay = splineBasedDecay<Float>(this)
val down = awaitFirstDown()
val offsetLimit = calculateOffsetLimit()
val dragHandler = { change: PointerInputChange ->
scope?.launch {
val dragChange = change.calculateDragChange(orientation)
dragOffset.snapTo(
(dragOffset.value - dragChange).coerceIn(
offsetLimit.min,
offsetLimit.max
)
)
updateIndex(dragOffset.value)
}
tracker.addPosition(change.uptimeMillis, change.position)
}
when (orientation) {
Orientation.Horizontal -> horizontalDrag(down.id, dragHandler)
Orientation.Vertical -> verticalDrag(down.id, dragHandler)
}
val velocity = tracker.calculateVelocity(orientation)
scope?.launch {
var targetOffset = decay.calculateTargetValue(dragOffset.value, -velocity)
val remainder = targetOffset.toInt().absoluteValue % itemDimension
val extra = if (remainder > itemDimension / 2f) 1 else 0
val lastVisibleIndex =
(targetOffset.absoluteValue / itemDimension.toFloat()).toInt() + extra
targetOffset =
(lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign)
.coerceIn(
0f,
(numberOfItems - 1).toFloat() * (itemDimension + itemSpacing)
)
dragOffset.animateTo(
animationSpec = animationSpec,
targetValue = targetOffset,
initialVelocity = -velocity
) {
updateIndex(value)
}
}
}
}
}
data class OffsetLimit(
val min: Float,
val max: Float,
)
}
private fun VelocityTracker.calculateVelocity(orientation: Orientation) = when (orientation) {
Orientation.Horizontal -> calculateVelocity().x
Orientation.Vertical -> calculateVelocity().y
}
private fun PointerInputChange.calculateDragChange(orientation: Orientation) =
when (orientation) {
Orientation.Horizontal -> positionChange().x
Orientation.Vertical -> positionChange().y
}

View File

@ -1,58 +0,0 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import kotlin.math.absoluteValue
@OptIn(ExperimentalPagerApi::class)
@Composable
fun BoxScope.MaskBox(
modifier: Modifier = Modifier,
pagerState: PagerState,
currentPage: Int = 0,
) {
val transition = updateTransition(targetState = pagerState, label = "")
val maskAlpha by transition.animateFloat(
label = "",
transitionSpec = {
spring()
}
) {
when {
it.targetPage == currentPage -> {
if (it.currentPage > currentPage) {
1f - it.currentPageOffset.absoluteValue
} else {
0f
}
}
it.targetPage > currentPage -> {
it.currentPageOffset.absoluteValue
}
else -> 0f
}
}
Box(
modifier
.alpha(maskAlpha)
) {
Box(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant)
)
}
}

View File

@ -1,97 +0,0 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.core.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import me.ash.reader.ui.extension.calculateTopBarAnimateValue
@Composable
fun BoxScope.TopTitleBox(
title: String,
description: String,
listState: LazyListState,
SpacerHeight: Float = Float.NaN,
startOffset: Offset,
startHeight: Float,
startTitleFontSize: Float,
startDescriptionFontSize: Float,
clickable: () -> Unit = {},
) {
val transition = updateTransition(targetState = listState, label = "")
val offset by transition.animateOffset(
label = "",
transitionSpec = { spring() }
) {
Offset(
x = it.calculateTopBarAnimateValue(startOffset.x, 56f),
y = it.calculateTopBarAnimateValue(startOffset.y, 0f)
)
}
val height by transition.animateFloat(
label = "",
transitionSpec = { spring() }
) {
it.calculateTopBarAnimateValue(startHeight, 64f)
}
val titleFontSize by transition.animateFloat(
label = "",
transitionSpec = { spring(stiffness = Spring.StiffnessHigh) }
) {
it.calculateTopBarAnimateValue(startTitleFontSize, 16f)
}
val descriptionFontSize by transition.animateFloat(
label = "",
transitionSpec = { spring(stiffness = Spring.StiffnessHigh) }
) {
it.calculateTopBarAnimateValue(startDescriptionFontSize, 12f)
}
Box(
modifier = Modifier
.zIndex(1f)
.height(height.dp)
.offset(offset.x.dp, offset.y.dp)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null,
onClickLabel = "回到顶部",
onClick = clickable
),
contentAlignment = Alignment.Center
) {
Column {
AnimatedText(
text = title,
fontWeight = FontWeight.Bold,
fontSize = titleFontSize.sp,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SpacerHeight.dp))
AnimatedText(
modifier = Modifier.width(200.dp),
text = description,
fontWeight = FontWeight.SemiBold,
fontSize = descriptionFontSize.sp,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}