Add subscribe a single feed feature
This commit is contained in:
parent
11ca1f1ae8
commit
1713331125
18
app/src/main/java/me/ash/reader/StringExt.kt
Normal file
18
app/src/main/java/me/ash/reader/StringExt.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package me.ash.reader
|
||||||
|
|
||||||
|
fun String.formatUrl(): String {
|
||||||
|
if (this.startsWith("//")) {
|
||||||
|
return "https:$this"
|
||||||
|
}
|
||||||
|
val regex = Regex("^(https?|ftp|file).*")
|
||||||
|
return if (!regex.matches(this)) {
|
||||||
|
"https://$this"
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.isUrl(): Boolean {
|
||||||
|
val regex = Regex("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]")
|
||||||
|
return regex.matches(this)
|
||||||
|
}
|
|
@ -263,7 +263,8 @@ interface ArticleDao {
|
||||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||||
a.accountId, a.isUnread, a.isStarred
|
a.accountId, a.isUnread, a.isStarred
|
||||||
FROM article AS a, feed AS b
|
FROM article AS a LEFT JOIN feed AS b
|
||||||
|
ON a.feedId = b.id
|
||||||
WHERE a.feedId = :feedId
|
WHERE a.feedId = :feedId
|
||||||
AND a.accountId = :accountId
|
AND a.accountId = :accountId
|
||||||
ORDER BY date DESC LIMIT 1
|
ORDER BY date DESC LIMIT 1
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package me.ash.reader.data.constant
|
package me.ash.reader.data.constant
|
||||||
|
|
||||||
object Symbol {
|
object Symbol {
|
||||||
const val NOTHING: String = "null"
|
const val NOTHING: String = "Null"
|
||||||
|
const val Unknown: String = "Unknown"
|
||||||
const val NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE: String = "article.update"
|
const val NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE: String = "article.update"
|
||||||
const val EXTRA_ARTICLE_ID: String = "article.id"
|
const val EXTRA_ARTICLE_ID: String = "article.id"
|
||||||
}
|
}
|
|
@ -23,10 +23,12 @@ data class Feed(
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val url: String,
|
val url: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
var groupId: Int,
|
var groupId: Int? = null,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
val accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
|
var isNotification: Boolean = false,
|
||||||
|
@ColumnInfo(defaultValue = "false")
|
||||||
var isFullContent: Boolean = false,
|
var isFullContent: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@ -47,6 +49,7 @@ data class Feed(
|
||||||
if (url != other.url) return false
|
if (url != other.url) return false
|
||||||
if (groupId != other.groupId) return false
|
if (groupId != other.groupId) return false
|
||||||
if (accountId != other.accountId) return false
|
if (accountId != other.accountId) return false
|
||||||
|
if (isNotification != other.isNotification) return false
|
||||||
if (isFullContent != other.isFullContent) return false
|
if (isFullContent != other.isFullContent) return false
|
||||||
if (important != other.important) return false
|
if (important != other.important) return false
|
||||||
|
|
||||||
|
@ -58,8 +61,9 @@ data class Feed(
|
||||||
result = 31 * result + name.hashCode()
|
result = 31 * result + name.hashCode()
|
||||||
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
||||||
result = 31 * result + url.hashCode()
|
result = 31 * result + url.hashCode()
|
||||||
result = 31 * result + groupId
|
result = 31 * result + (groupId ?: 0)
|
||||||
result = 31 * result + accountId
|
result = 31 * result + accountId
|
||||||
|
result = 31 * result + isNotification.hashCode()
|
||||||
result = 31 * result + isFullContent.hashCode()
|
result = 31 * result + isFullContent.hashCode()
|
||||||
result = 31 * result + (important ?: 0)
|
result = 31 * result + (important ?: 0)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -13,7 +13,10 @@ interface FeedDao {
|
||||||
suspend fun queryAll(accountId: Int): List<Feed>
|
suspend fun queryAll(accountId: Int): List<Feed>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertList(feed: List<Feed>): List<Long>
|
suspend fun insert(feed: Feed): Long
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertList(feeds: List<Feed>): List<Long>
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(vararg feed: Feed)
|
suspend fun update(vararg feed: Feed)
|
||||||
|
|
|
@ -14,6 +14,14 @@ interface GroupDao {
|
||||||
)
|
)
|
||||||
fun queryAllGroupWithFeed(accountId: Int): Flow<MutableList<GroupWithFeed>>
|
fun queryAllGroupWithFeed(accountId: Int): Flow<MutableList<GroupWithFeed>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM `group`
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insert(group: Group): Long
|
suspend fun insert(group: Group): Long
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,9 @@ import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.article.ArticleDao
|
import me.ash.reader.data.article.ArticleDao
|
||||||
import me.ash.reader.data.article.ArticleWithFeed
|
import me.ash.reader.data.article.ArticleWithFeed
|
||||||
import me.ash.reader.data.article.ImportantCount
|
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.GroupDao
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.dataStore
|
import me.ash.reader.dataStore
|
||||||
|
@ -21,7 +24,12 @@ class ArticleRepository @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val articleDao: ArticleDao,
|
private val articleDao: ArticleDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
|
private val feedDao: FeedDao,
|
||||||
) {
|
) {
|
||||||
|
fun pullGroups(): Flow<MutableList<Group>> {
|
||||||
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
||||||
|
return groupDao.queryAllGroup(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
||||||
return groupDao.queryAllGroupWithFeed(
|
return groupDao.queryAllGroupWithFeed(
|
||||||
|
@ -90,4 +98,11 @@ class ArticleRepository @Inject constructor(
|
||||||
suspend fun findArticleById(id: Int): ArticleWithFeed? {
|
suspend fun findArticleById(id: Int): ArticleWithFeed? {
|
||||||
return articleDao.queryById(id)
|
return articleDao.queryById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||||
|
val feedId = feedDao.insert(feed).toInt()
|
||||||
|
articleDao.insertList(articles.map {
|
||||||
|
it.copy(feedId = feedId)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -11,7 +11,6 @@ import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.github.muhrifqii.parserss.ParseRSS
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
@ -25,12 +24,12 @@ import me.ash.reader.data.article.ArticleDao
|
||||||
import me.ash.reader.data.constant.Symbol
|
import me.ash.reader.data.constant.Symbol
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
|
import me.ash.reader.data.feed.FeedWithArticle
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
import me.ash.reader.data.source.ReaderDatabase
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import net.dankito.readability4j.Readability4J
|
import net.dankito.readability4j.Readability4J
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -46,6 +45,38 @@ class RssRepository @Inject constructor(
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
) {
|
) {
|
||||||
|
@Throws(Exception::class)
|
||||||
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
||||||
|
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
||||||
|
val feed = Feed(
|
||||||
|
name = parseRss.title!!,
|
||||||
|
url = feedLink,
|
||||||
|
groupId = 0,
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
val articles = mutableListOf<Article>()
|
||||||
|
parseRss.items.forEach {
|
||||||
|
articles.add(
|
||||||
|
Article(
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = feed.id ?: 0,
|
||||||
|
date = Date(it.publishDate.toString()),
|
||||||
|
title = it.title.toString(),
|
||||||
|
author = it.author,
|
||||||
|
rawDescription = it.description.toString(),
|
||||||
|
shortDescription = (Readability4JExtended("", it.description.toString())
|
||||||
|
.parse().textContent ?: "").trim().run {
|
||||||
|
if (this.length > 100) this.substring(0, 100)
|
||||||
|
else this
|
||||||
|
},
|
||||||
|
link = it.link ?: "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return FeedWithArticle(feed, articles)
|
||||||
|
}
|
||||||
|
|
||||||
fun parseDescriptionContent(link: String, content: String): String {
|
fun parseDescriptionContent(link: String, content: String): String {
|
||||||
val readability4J: Readability4J = Readability4JExtended(link, content)
|
val readability4J: Readability4J = Readability4JExtended(link, content)
|
||||||
val article = readability4J.parse()
|
val article = readability4J.parse()
|
||||||
|
@ -146,6 +177,10 @@ class RssRepository @Inject constructor(
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
||||||
?: return
|
?: return
|
||||||
val feeds = feedDao.queryAll(accountId)
|
val feeds = feedDao.queryAll(accountId)
|
||||||
|
val feedNotificationMap = mutableMapOf<Int, Boolean>()
|
||||||
|
feeds.forEach { feed ->
|
||||||
|
feedNotificationMap[feed.id ?: 0] = feed.isNotification
|
||||||
|
}
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val chunked = feeds.chunked(6)
|
val chunked = feeds.chunked(6)
|
||||||
chunked.forEachIndexed { index, item ->
|
chunked.forEachIndexed { index, item ->
|
||||||
|
@ -199,15 +234,15 @@ class RssRepository @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
it.reversed().forEach { articleList ->
|
it.forEach { articleList ->
|
||||||
val ids = articleDao.insertList(articleList)
|
val ids = articleDao.insertList(articleList)
|
||||||
articleList.forEachIndexed { index, article ->
|
articleList.forEachIndexed { index, article ->
|
||||||
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
||||||
|
if (feedNotificationMap[article.feedId] == true) {
|
||||||
val builder = NotificationCompat.Builder(
|
val builder = NotificationCompat.Builder(
|
||||||
context,
|
context,
|
||||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
||||||
)
|
).setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
|
||||||
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||||
.setContentTitle(article.title)
|
.setContentTitle(article.title)
|
||||||
.setContentText(article.shortDescription)
|
.setContentText(article.shortDescription)
|
||||||
|
@ -219,14 +254,21 @@ class RssRepository @Inject constructor(
|
||||||
Intent(context, MainActivity::class.java).apply {
|
Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
putExtra(Symbol.EXTRA_ARTICLE_ID, ids[index].toInt())
|
putExtra(
|
||||||
|
Symbol.EXTRA_ARTICLE_ID,
|
||||||
|
ids[index].toInt()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
notificationManager.notify(ids[index].toInt(), builder.build().apply {
|
notificationManager.notify(
|
||||||
|
ids[index].toInt(),
|
||||||
|
builder.build().apply {
|
||||||
flags = Notification.FLAG_AUTO_CANCEL
|
flags = Notification.FLAG_AUTO_CANCEL
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.buffer().onCompletion {
|
}.buffer().onCompletion {
|
||||||
|
@ -256,7 +298,6 @@ class RssRepository @Inject constructor(
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
latestTitle: String? = null,
|
latestTitle: String? = null,
|
||||||
): List<Article> {
|
): List<Article> {
|
||||||
ParseRSS.init(XmlPullParserFactory.newInstance())
|
|
||||||
val a = mutableListOf<Article>()
|
val a = mutableListOf<Article>()
|
||||||
try {
|
try {
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package me.ash.reader.data.source
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
import com.github.muhrifqii.parserss.ParseRSS
|
||||||
import com.github.muhrifqii.parserss.RSSFeedObject
|
import com.github.muhrifqii.parserss.RSSFeedObject
|
||||||
import com.github.muhrifqii.parserss.retrofit.ParseRSSConverterFactory
|
import com.github.muhrifqii.parserss.retrofit.ParseRSSConverterFactory
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Url
|
import retrofit2.http.Url
|
||||||
|
@ -15,6 +17,7 @@ interface RssNetworkDataSource {
|
||||||
|
|
||||||
fun getInstance(): RssNetworkDataSource {
|
fun getInstance(): RssNetworkDataSource {
|
||||||
return instance ?: synchronized(this) {
|
return instance ?: synchronized(this) {
|
||||||
|
ParseRSS.init(XmlPullParserFactory.newInstance())
|
||||||
instance ?: Retrofit.Builder()
|
instance ?: Retrofit.Builder()
|
||||||
.baseUrl("https://api.feeddd.org/feeds/")
|
.baseUrl("https://api.feeddd.org/feeds/")
|
||||||
.addConverterFactory(ParseRSSConverterFactory.create<RSSFeedObject>())
|
.addConverterFactory(ParseRSSConverterFactory.create<RSSFeedObject>())
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package me.ash.reader.ui.extension
|
||||||
|
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
fun PagerState.animateScrollToPage(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
targetPage: Int,
|
||||||
|
callback: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
scope.launch {
|
||||||
|
animateScrollToPage(targetPage)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import me.ash.reader.data.constant.Filter
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
|
import me.ash.reader.ui.extension.animateScrollToPage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@ -62,10 +63,7 @@ class HomeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollToPage(scope: CoroutineScope, targetPage: Int, callback: () -> Unit = {}) {
|
private fun scrollToPage(scope: CoroutineScope, targetPage: Int, callback: () -> Unit = {}) {
|
||||||
scope.launch {
|
_viewState.value.pagerState.animateScrollToPage(scope, targetPage, callback)
|
||||||
_viewState.value.pagerState.animateScrollToPage(targetPage)
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@ data class FilterState(
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
data class HomeViewState constructor(
|
data class HomeViewState(
|
||||||
val pagerState: PagerState = PagerState(1),
|
val pagerState: PagerState = PagerState(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.article
|
package me.ash.reader.ui.page.home.article
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||||
import androidx.compose.material.icons.rounded.DoneAll
|
import androidx.compose.material.icons.rounded.DoneAll
|
||||||
|
@ -9,6 +10,7 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArticlePageTopBar(
|
fun ArticlePageTopBar(
|
||||||
|
@ -16,10 +18,14 @@ fun ArticlePageTopBar(
|
||||||
readAllOnClick: () -> Unit = {},
|
readAllOnClick: () -> Unit = {},
|
||||||
searchOnClick: () -> Unit = {},
|
searchOnClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = backOnClick) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
backOnClick()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.ArrowBackIosNew,
|
imageVector = Icons.Rounded.ArrowBackIosNew,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
|
@ -28,14 +34,20 @@ fun ArticlePageTopBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = readAllOnClick) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
readAllOnClick()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.DoneAll,
|
imageVector = Icons.Rounded.DoneAll,
|
||||||
contentDescription = "Done All",
|
contentDescription = "Done All",
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = searchOnClick) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
searchOnClick()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Search,
|
imageVector = Icons.Rounded.Search,
|
||||||
contentDescription = "Search",
|
contentDescription = "Search",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
package me.ash.reader.ui.page.home.feed
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
@ -24,7 +23,6 @@ fun ColumnScope.FeedList(
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.animateContentSize()) {
|
Column(modifier = Modifier.animateContentSize()) {
|
||||||
feeds.forEach { feed ->
|
feeds.forEach { feed ->
|
||||||
Log.i("RLog", "FeedList: ${feed.icon}")
|
|
||||||
FeedBar(
|
FeedBar(
|
||||||
barButtonType = ItemType(
|
barButtonType = ItemType(
|
||||||
// icon = feed.icon ?: "",
|
// icon = feed.icon ?: "",
|
||||||
|
|
|
@ -23,6 +23,8 @@ import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.page.home.HomeViewAction
|
import me.ash.reader.ui.page.home.HomeViewAction
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
|
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
|
||||||
|
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewAction
|
||||||
|
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewModel
|
||||||
import me.ash.reader.ui.widget.TopTitleBox
|
import me.ash.reader.ui.widget.TopTitleBox
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -31,6 +33,7 @@ fun FeedPage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: FeedViewModel = hiltViewModel(),
|
viewModel: FeedViewModel = hiltViewModel(),
|
||||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||||
|
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
||||||
) {
|
) {
|
||||||
|
@ -59,21 +62,6 @@ fun FeedPage(
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
SubscribeDialog(
|
SubscribeDialog(
|
||||||
visible = viewState.subscribeDialogVisible,
|
|
||||||
hiddenFunction = {
|
|
||||||
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(false))
|
|
||||||
},
|
|
||||||
inputContent = viewState.subscribeDialogFeedLink,
|
|
||||||
onValueChange = {
|
|
||||||
viewModel.dispatch(
|
|
||||||
FeedViewAction.InputSubscribeFeedLink(it)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onKeyboardAction = {
|
|
||||||
viewModel.dispatch(
|
|
||||||
FeedViewAction.ChangeSubscribeDialogVisible(false)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
openInputStreamCallback = {
|
openInputStreamCallback = {
|
||||||
viewModel.dispatch(FeedViewAction.AddFromFile(it))
|
viewModel.dispatch(FeedViewAction.AddFromFile(it))
|
||||||
},
|
},
|
||||||
|
@ -102,7 +90,7 @@ fun FeedPage(
|
||||||
homeViewModel.dispatch(HomeViewAction.Sync())
|
homeViewModel.dispatch(HomeViewAction.Sync())
|
||||||
},
|
},
|
||||||
subscribeOnClick = {
|
subscribeOnClick = {
|
||||||
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(true))
|
subscribeViewModel.dispatch(SubscribeViewAction.Show)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
package me.ash.reader.ui.page.home.feed
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
@ -11,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
|
@ -22,10 +24,12 @@ fun FeedPageTopBar(
|
||||||
syncOnClick: () -> Unit = {},
|
syncOnClick: () -> Unit = {},
|
||||||
subscribeOnClick: () -> Unit = {},
|
subscribeOnClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
navController.navigate(route = RouteName.SETTINGS)
|
navController.navigate(route = RouteName.SETTINGS)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -38,6 +42,7 @@ fun FeedPageTopBar(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
if (isSyncing) return@IconButton
|
if (isSyncing) return@IconButton
|
||||||
syncOnClick()
|
syncOnClick()
|
||||||
}) {
|
}) {
|
||||||
|
@ -48,7 +53,10 @@ fun FeedPageTopBar(
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = subscribeOnClick) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
subscribeOnClick()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(26.dp),
|
modifier = Modifier.size(26.dp),
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
|
|
|
@ -33,28 +33,6 @@ class FeedViewModel @Inject constructor(
|
||||||
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
||||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
|
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
|
||||||
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
|
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||||
is FeedViewAction.ChangeSubscribeDialogVisible -> changeAddFeedDialogVisible(action.visible)
|
|
||||||
is FeedViewAction.InputSubscribeFeedLink -> inputSubscribeFeedLink(action.subscribeFeedLink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun inputSubscribeFeedLink(subscribeFeedLink: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
|
||||||
subscribeDialogFeedLink = subscribeFeedLink
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeAddFeedDialogVisible(visible: Boolean) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
|
||||||
subscribeDialogVisible = visible
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,8 +143,6 @@ data class FeedViewState(
|
||||||
val feedsVisible: List<Boolean> = emptyList(),
|
val feedsVisible: List<Boolean> = emptyList(),
|
||||||
val listState: LazyListState = LazyListState(),
|
val listState: LazyListState = LazyListState(),
|
||||||
val groupsVisible: Boolean = true,
|
val groupsVisible: Boolean = true,
|
||||||
var subscribeDialogVisible: Boolean = false,
|
|
||||||
var subscribeDialogFeedLink: String = "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class FeedViewAction {
|
sealed class FeedViewAction {
|
||||||
|
@ -194,12 +170,4 @@ sealed class FeedViewAction {
|
||||||
data class ScrollToItem(
|
data class ScrollToItem(
|
||||||
val index: Int
|
val index: Int
|
||||||
) : FeedViewAction()
|
) : FeedViewAction()
|
||||||
|
|
||||||
data class ChangeSubscribeDialogVisible(
|
|
||||||
val visible: Boolean
|
|
||||||
) : FeedViewAction()
|
|
||||||
|
|
||||||
data class InputSubscribeFeedLink(
|
|
||||||
val subscribeFeedLink: String
|
|
||||||
) : FeedViewAction()
|
|
||||||
}
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package me.ash.reader.ui.page.home.feed.subscribe
|
package me.ash.reader.ui.page.home.feed.subscribe
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Article
|
import androidx.compose.material.icons.outlined.Article
|
||||||
|
@ -17,31 +19,56 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.flowlayout.MainAxisAlignment
|
import com.google.accompanist.flowlayout.MainAxisAlignment
|
||||||
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.ui.widget.SelectionChip
|
import me.ash.reader.ui.widget.SelectionChip
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResultViewPage() {
|
fun ResultViewPage(
|
||||||
|
link: String = "",
|
||||||
|
groups: List<Group> = emptyList(),
|
||||||
|
selectedNotificationPreset: Boolean = false,
|
||||||
|
selectedFullContentParsePreset: Boolean = false,
|
||||||
|
selectedGroupId: Int = 0,
|
||||||
|
notificationPresetOnClick: () -> Unit = {},
|
||||||
|
fullContentParsePresetOnClick: () -> Unit = {},
|
||||||
|
groupOnClick: (groupId: Int) -> Unit = {},
|
||||||
|
onKeyboardAction: () -> Unit = {},
|
||||||
|
) {
|
||||||
Column {
|
Column {
|
||||||
Link()
|
Link(
|
||||||
|
text = link
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(26.dp))
|
Spacer(modifier = Modifier.height(26.dp))
|
||||||
|
|
||||||
Preset()
|
Preset(
|
||||||
|
selectedNotificationPreset = selectedNotificationPreset,
|
||||||
|
selectedFullContentParsePreset = selectedFullContentParsePreset,
|
||||||
|
notificationPresetOnClick = notificationPresetOnClick,
|
||||||
|
fullContentParsePresetOnClick = fullContentParsePresetOnClick,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(26.dp))
|
Spacer(modifier = Modifier.height(26.dp))
|
||||||
|
|
||||||
AddToGroup()
|
AddToGroup(
|
||||||
|
groups = groups,
|
||||||
|
selectedGroupId = selectedGroupId,
|
||||||
|
groupOnClick = groupOnClick,
|
||||||
|
onKeyboardAction = onKeyboardAction,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Link() {
|
private fun Link(
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
text = "https://material.io/feed.xml",
|
text = text,
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +76,12 @@ private fun Link() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preset() {
|
private fun Preset(
|
||||||
|
selectedNotificationPreset: Boolean = false,
|
||||||
|
selectedFullContentParsePreset: Boolean = false,
|
||||||
|
notificationPresetOnClick: () -> Unit = {},
|
||||||
|
fullContentParsePresetOnClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "预设",
|
text = "预设",
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
@ -62,7 +94,7 @@ private fun Preset() {
|
||||||
mainAxisSpacing = 10.dp,
|
mainAxisSpacing = 10.dp,
|
||||||
) {
|
) {
|
||||||
SelectionChip(
|
SelectionChip(
|
||||||
selected = true,
|
selected = selectedNotificationPreset,
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Notifications,
|
imageVector = Icons.Outlined.Notifications,
|
||||||
|
@ -70,7 +102,7 @@ private fun Preset() {
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { /*TODO*/ },
|
onClick = notificationPresetOnClick,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "接收通知",
|
text = "接收通知",
|
||||||
|
@ -79,7 +111,7 @@ private fun Preset() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SelectionChip(
|
SelectionChip(
|
||||||
selected = false,
|
selected = selectedFullContentParsePreset,
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Article,
|
imageVector = Icons.Outlined.Article,
|
||||||
|
@ -87,10 +119,10 @@ private fun Preset() {
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { /*TODO*/ }
|
onClick = fullContentParsePresetOnClick,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "全文输出",
|
text = "全文解析",
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
|
@ -99,7 +131,12 @@ private fun Preset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AddToGroup() {
|
private fun AddToGroup(
|
||||||
|
groups: List<Group>,
|
||||||
|
selectedGroupId: Int,
|
||||||
|
groupOnClick: (groupId: Int) -> Unit = {},
|
||||||
|
onKeyboardAction: () -> Unit = {},
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "添加到组",
|
text = "添加到组",
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
@ -111,49 +148,23 @@ private fun AddToGroup() {
|
||||||
crossAxisSpacing = 10.dp,
|
crossAxisSpacing = 10.dp,
|
||||||
mainAxisSpacing = 10.dp,
|
mainAxisSpacing = 10.dp,
|
||||||
) {
|
) {
|
||||||
|
groups.forEach {
|
||||||
|
SelectionChip(
|
||||||
|
modifier = Modifier.animateContentSize(),
|
||||||
|
selected = it.id == selectedGroupId,
|
||||||
|
onClick = { groupOnClick(it.id ?: 0) },
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it.name,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SelectionChip(
|
SelectionChip(
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = { /*TODO*/ },
|
onClick = { /*TODO*/ },
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "未分组",
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SelectionChip(
|
|
||||||
selected = true,
|
|
||||||
onClick = { /*TODO*/ }
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "技术",
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SelectionChip(
|
|
||||||
selected = true,
|
|
||||||
onClick = { /*TODO*/ }
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "新鲜事",
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SelectionChip(
|
|
||||||
selected = false,
|
|
||||||
onClick = { /*TODO*/ }
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "游戏",
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SelectionChip(
|
|
||||||
selected = true,
|
|
||||||
onClick = { /*TODO*/ },
|
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = Modifier.width(56.dp),
|
modifier = Modifier.width(56.dp),
|
||||||
|
@ -165,6 +176,11 @@ private fun AddToGroup() {
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
||||||
),
|
),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
onKeyboardAction()
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package me.ash.reader.ui.page.home.feed.subscribe
|
package me.ash.reader.ui.page.home.feed.subscribe
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.TextField
|
import androidx.compose.material.TextField
|
||||||
import androidx.compose.material.TextFieldDefaults
|
import androidx.compose.material.TextFieldDefaults
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material.icons.rounded.ContentPaste
|
import androidx.compose.material.icons.rounded.ContentPaste
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
@ -24,6 +28,7 @@ import kotlinx.coroutines.delay
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchViewPage(
|
fun SearchViewPage(
|
||||||
inputContent: String = "",
|
inputContent: String = "",
|
||||||
|
errorMessage: String = "",
|
||||||
onValueChange: (String) -> Unit = {},
|
onValueChange: (String) -> Unit = {},
|
||||||
onKeyboardAction: () -> Unit = {},
|
onKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
@ -34,6 +39,7 @@ fun SearchViewPage(
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
@ -53,10 +59,21 @@ fun SearchViewPage(
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
isError = errorMessage.isNotEmpty(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
if (inputContent.isNotEmpty()) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
onValueChange("")
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = "Clear",
|
||||||
|
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
// focusRequester.requestFocus()
|
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.ContentPaste,
|
imageVector = Icons.Rounded.ContentPaste,
|
||||||
|
@ -64,6 +81,7 @@ fun SearchViewPage(
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
|
@ -71,5 +89,17 @@ fun SearchViewPage(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (errorMessage.isNotEmpty()) {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
|
}
|
|
@ -8,22 +8,23 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.widget.Dialog
|
import me.ash.reader.ui.widget.Dialog
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubscribeDialog(
|
fun SubscribeDialog(
|
||||||
visible: Boolean,
|
viewModel: SubscribeViewModel = hiltViewModel(),
|
||||||
hiddenFunction: () -> Unit,
|
|
||||||
inputContent: String = "",
|
|
||||||
onValueChange: (String) -> Unit = {},
|
|
||||||
onKeyboardAction: () -> Unit = {},
|
|
||||||
openInputStreamCallback: (InputStream) -> Unit,
|
openInputStreamCallback: (InputStream) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||||
it?.let { uri ->
|
it?.let { uri ->
|
||||||
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
||||||
|
@ -31,50 +32,124 @@ fun SubscribeDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
|
val groupsState = viewState.groups.collectAsState(initial = emptyList())
|
||||||
|
var height by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(viewState.visible) {
|
||||||
|
if (viewState.visible) {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Init)
|
||||||
|
} else {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Reset)
|
||||||
|
viewState.pagerState.scrollToPage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
visible = visible,
|
visible = viewState.visible,
|
||||||
onDismissRequest = hiddenFunction,
|
onDismissRequest = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||||
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.RssFeed,
|
imageVector = Icons.Rounded.RssFeed,
|
||||||
contentDescription = "Subscribe",
|
contentDescription = "Subscribe",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = { Text("订阅") },
|
title = {
|
||||||
|
Text(
|
||||||
|
when (viewState.pagerState.currentPage) {
|
||||||
|
0 -> "订阅"
|
||||||
|
else -> viewState.feed?.name ?: "未知"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
SubscribeViewPager(
|
SubscribeViewPager(
|
||||||
inputContent = inputContent,
|
// height = when (viewState.pagerState.currentPage) {
|
||||||
onValueChange = onValueChange,
|
// 0 -> 84.dp
|
||||||
onKeyboardAction = onKeyboardAction,
|
// else -> Dp.Unspecified
|
||||||
|
// },
|
||||||
|
inputContent = viewState.inputContent,
|
||||||
|
errorMessage = viewState.errorMessage,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Input(it))
|
||||||
|
},
|
||||||
|
onSearchKeyboardAction = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Search(scope))
|
||||||
|
},
|
||||||
|
link = viewState.inputContent,
|
||||||
|
groups = groupsState.value,
|
||||||
|
selectedNotificationPreset = viewState.notificationPreset,
|
||||||
|
selectedFullContentParsePreset = viewState.fullContentParsePreset,
|
||||||
|
selectedGroupId = viewState.selectedGroupId ?: 0,
|
||||||
|
pagerState = viewState.pagerState,
|
||||||
|
notificationPresetOnClick = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.ChangeNotificationPreset)
|
||||||
|
},
|
||||||
|
fullContentParsePresetOnClick = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.ChangeFullContentParsePreset)
|
||||||
|
},
|
||||||
|
groupOnClick = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
|
||||||
|
},
|
||||||
|
onResultKeyboardAction = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Subscribe)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
when (viewState.pagerState.currentPage) {
|
||||||
|
0 -> {
|
||||||
TextButton(
|
TextButton(
|
||||||
enabled = inputContent.isNotEmpty(),
|
enabled = viewState.inputContent.isNotEmpty(),
|
||||||
onClick = {
|
onClick = {
|
||||||
hiddenFunction()
|
viewModel.dispatch(SubscribeViewAction.Search(scope))
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "搜索",
|
text = "搜索",
|
||||||
color = if (inputContent.isNotEmpty()) {
|
color = if (viewState.inputContent.isNotEmpty()) {
|
||||||
Color.Unspecified
|
Color.Unspecified
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Subscribe)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("订阅")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
when (viewState.pagerState.currentPage) {
|
||||||
|
0 -> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
launcher.launch("*/*")
|
launcher.launch("*/*")
|
||||||
hiddenFunction()
|
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("导入OPML文件")
|
Text("导入OPML文件")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
package me.ash.reader.ui.page.home.feed.subscribe
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.article.Article
|
||||||
|
import me.ash.reader.data.constant.Symbol
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.data.group.Group
|
||||||
|
import me.ash.reader.data.repository.ArticleRepository
|
||||||
|
import me.ash.reader.data.repository.RssRepository
|
||||||
|
import me.ash.reader.formatUrl
|
||||||
|
import me.ash.reader.ui.extension.animateScrollToPage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@HiltViewModel
|
||||||
|
class SubscribeViewModel @Inject constructor(
|
||||||
|
private val articleRepository: ArticleRepository,
|
||||||
|
private val rssRepository: RssRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _viewState = MutableStateFlow(SubScribeViewState())
|
||||||
|
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow()
|
||||||
|
|
||||||
|
fun dispatch(action: SubscribeViewAction) {
|
||||||
|
when (action) {
|
||||||
|
is SubscribeViewAction.Init -> init()
|
||||||
|
is SubscribeViewAction.Reset -> reset()
|
||||||
|
is SubscribeViewAction.Show -> changeVisible(true)
|
||||||
|
is SubscribeViewAction.Hide -> changeVisible(false)
|
||||||
|
is SubscribeViewAction.Input -> inputLink(action.content)
|
||||||
|
is SubscribeViewAction.Search -> search(action.scope)
|
||||||
|
is SubscribeViewAction.ChangeNotificationPreset ->
|
||||||
|
changeNotificationPreset()
|
||||||
|
is SubscribeViewAction.ChangeFullContentParsePreset ->
|
||||||
|
changeFullContentParsePreset()
|
||||||
|
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
|
||||||
|
is SubscribeViewAction.Subscribe -> subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
groups = articleRepository.pullGroups()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
visible = false,
|
||||||
|
title = "订阅",
|
||||||
|
errorMessage = "",
|
||||||
|
inputContent = "",
|
||||||
|
feed = null,
|
||||||
|
articles = emptyList(),
|
||||||
|
notificationPreset = false,
|
||||||
|
fullContentParsePreset = false,
|
||||||
|
selectedGroupId = null,
|
||||||
|
groups = emptyFlow(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribe() {
|
||||||
|
val feed = _viewState.value.feed ?: return
|
||||||
|
val articles = _viewState.value.articles
|
||||||
|
val groupId = _viewState.value.selectedGroupId ?: 0
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
articleRepository.subscribe(
|
||||||
|
feed.copy(
|
||||||
|
groupId = groupId,
|
||||||
|
isNotification = _viewState.value.notificationPreset,
|
||||||
|
isFullContent = _viewState.value.fullContentParsePreset,
|
||||||
|
), articles
|
||||||
|
)
|
||||||
|
changeVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectedGroup(groupId: Int? = null) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
selectedGroupId = groupId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeFullContentParsePreset() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
fullContentParsePreset = !_viewState.value.fullContentParsePreset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeNotificationPreset() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
notificationPreset = !_viewState.value.notificationPreset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(scope: CoroutineScope) {
|
||||||
|
_viewState.value.inputContent.formatUrl().let { str ->
|
||||||
|
if (str != _viewState.value.inputContent) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
inputContent = str
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
title = "搜索中",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val feedWithArticle = rssRepository.searchFeed(_viewState.value.inputContent)
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
feed = feedWithArticle.feed,
|
||||||
|
articles = feedWithArticle.articles,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_viewState.value.pagerState.animateScrollToPage(scope, 1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
title = "订阅",
|
||||||
|
errorMessage = e.message ?: Symbol.Unknown,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inputLink(content: String) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
inputContent = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeVisible(visible: Boolean) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
visible = visible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
data class SubScribeViewState(
|
||||||
|
val visible: Boolean = false,
|
||||||
|
val title: String = "订阅",
|
||||||
|
val errorMessage: String = "",
|
||||||
|
val inputContent: String = "",
|
||||||
|
val feed: Feed? = null,
|
||||||
|
val articles: List<Article> = emptyList(),
|
||||||
|
val notificationPreset: Boolean = false,
|
||||||
|
val fullContentParsePreset: Boolean = false,
|
||||||
|
val selectedGroupId: Int? = null,
|
||||||
|
val groups: Flow<List<Group>> = emptyFlow(),
|
||||||
|
val pagerState: PagerState = PagerState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class SubscribeViewAction {
|
||||||
|
object Init : SubscribeViewAction()
|
||||||
|
object Reset : SubscribeViewAction()
|
||||||
|
|
||||||
|
object Show : SubscribeViewAction()
|
||||||
|
object Hide : SubscribeViewAction()
|
||||||
|
|
||||||
|
data class Input(
|
||||||
|
val content: String
|
||||||
|
) : SubscribeViewAction()
|
||||||
|
|
||||||
|
data class Search(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
) : SubscribeViewAction()
|
||||||
|
|
||||||
|
object ChangeNotificationPreset : SubscribeViewAction()
|
||||||
|
object ChangeFullContentParsePreset : SubscribeViewAction()
|
||||||
|
|
||||||
|
data class SelectedGroup(
|
||||||
|
val groupId: Int? = null
|
||||||
|
) : SubscribeViewAction()
|
||||||
|
|
||||||
|
object Subscribe : SubscribeViewAction()
|
||||||
|
}
|
|
@ -1,27 +1,58 @@
|
||||||
package me.ash.reader.ui.page.home.feed.subscribe
|
package me.ash.reader.ui.page.home.feed.subscribe
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.ui.widget.ViewPager
|
import me.ash.reader.ui.widget.ViewPager
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubscribeViewPager(
|
fun SubscribeViewPager(
|
||||||
|
height: Dp = Dp.Unspecified,
|
||||||
inputContent: String = "",
|
inputContent: String = "",
|
||||||
|
errorMessage: String = "",
|
||||||
onValueChange: (String) -> Unit = {},
|
onValueChange: (String) -> Unit = {},
|
||||||
onKeyboardAction: () -> Unit = {}
|
onSearchKeyboardAction: () -> Unit = {},
|
||||||
|
link: String = "",
|
||||||
|
groups: List<Group> = emptyList(),
|
||||||
|
selectedNotificationPreset: Boolean = false,
|
||||||
|
selectedFullContentParsePreset: Boolean = false,
|
||||||
|
selectedGroupId: Int = 0,
|
||||||
|
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
||||||
|
notificationPresetOnClick: () -> Unit = {},
|
||||||
|
fullContentParsePresetOnClick: () -> Unit = {},
|
||||||
|
groupOnClick: (groupId: Int) -> Unit = {},
|
||||||
|
onResultKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
ViewPager(
|
ViewPager(
|
||||||
|
modifier = Modifier.height(height),
|
||||||
|
state = pagerState,
|
||||||
|
userScrollEnabled = false,
|
||||||
composableList = listOf(
|
composableList = listOf(
|
||||||
{
|
{
|
||||||
SearchViewPage(
|
SearchViewPage(
|
||||||
inputContent = inputContent,
|
inputContent = inputContent,
|
||||||
|
errorMessage = errorMessage,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
onKeyboardAction = onKeyboardAction,
|
onKeyboardAction = onSearchKeyboardAction,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ResultViewPage()
|
ResultViewPage(
|
||||||
|
link = link,
|
||||||
|
groups = groups,
|
||||||
|
selectedNotificationPreset = selectedNotificationPreset,
|
||||||
|
selectedFullContentParsePreset = selectedFullContentParsePreset,
|
||||||
|
selectedGroupId = selectedGroupId,
|
||||||
|
notificationPresetOnClick = notificationPresetOnClick,
|
||||||
|
fullContentParsePresetOnClick = fullContentParsePresetOnClick,
|
||||||
|
groupOnClick = groupOnClick,
|
||||||
|
onKeyboardAction = onResultKeyboardAction,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package me.ash.reader.ui.page.home.read
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -74,7 +73,6 @@ fun ReadPage(
|
||||||
exit = fadeOut() + shrinkVertically(),
|
exit = fadeOut() + shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
if (viewState.articleWithFeed == null) return@AnimatedVisibility
|
if (viewState.articleWithFeed == null) return@AnimatedVisibility
|
||||||
SelectionContainer {
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = viewState.listState,
|
state = viewState.listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -105,4 +103,3 @@ fun ReadPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
package me.ash.reader.ui.page.home.read
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
@ -11,16 +12,21 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReadPageTopBar(
|
fun ReadPageTopBar(
|
||||||
btnBackOnClickListener: () -> Unit = {},
|
btnBackOnClickListener: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { btnBackOnClickListener() }) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
btnBackOnClickListener()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
imageVector = Icons.Rounded.Close,
|
imageVector = Icons.Rounded.Close,
|
||||||
|
@ -30,19 +36,23 @@ fun ReadPageTopBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { /*TODO*/ }) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Rounded.Share,
|
imageVector = Icons.Rounded.Share,
|
||||||
contentDescription = "Add",
|
contentDescription = "Share",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { /*TODO*/ }) {
|
IconButton(onClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
imageVector = Icons.Rounded.MoreHoriz,
|
imageVector = Icons.Rounded.MoreHoriz,
|
||||||
contentDescription = "Add",
|
contentDescription = "More",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package me.ash.reader.ui.widget
|
package me.ash.reader.ui.widget
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@ -14,11 +13,12 @@ fun Dialog(
|
||||||
confirmButton: @Composable () -> Unit,
|
confirmButton: @Composable () -> Unit,
|
||||||
dismissButton: @Composable (() -> Unit)? = null,
|
dismissButton: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
// AnimatedVisibility(
|
||||||
visible = visible,
|
// visible = visible,
|
||||||
enter = fadeIn() + expandVertically(),
|
// enter = fadeIn() + expandVertically(),
|
||||||
exit = fadeOut() + shrinkVertically(),
|
// exit = fadeOut() + shrinkVertically(),
|
||||||
) {
|
// ) {
|
||||||
|
if (visible) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
icon = icon,
|
icon = icon,
|
||||||
|
|
|
@ -16,7 +16,7 @@ fun ViewPager(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
state: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
||||||
composableList: List<@Composable () -> Unit>,
|
composableList: List<@Composable () -> Unit>,
|
||||||
userScrollEnabled: Boolean = true
|
userScrollEnabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
count = composableList.size,
|
count = composableList.size,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user