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,
|
||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||
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
|
||||
AND a.accountId = :accountId
|
||||
ORDER BY date DESC LIMIT 1
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package me.ash.reader.data.constant
|
||||
|
||||
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 EXTRA_ARTICLE_ID: String = "article.id"
|
||||
}
|
|
@ -23,10 +23,12 @@ data class Feed(
|
|||
@ColumnInfo
|
||||
val url: String,
|
||||
@ColumnInfo(index = true)
|
||||
var groupId: Int,
|
||||
var groupId: Int? = null,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
var isNotification: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
var isFullContent: Boolean = false,
|
||||
) {
|
||||
@Ignore
|
||||
|
@ -47,6 +49,7 @@ data class Feed(
|
|||
if (url != other.url) return false
|
||||
if (groupId != other.groupId) return false
|
||||
if (accountId != other.accountId) return false
|
||||
if (isNotification != other.isNotification) return false
|
||||
if (isFullContent != other.isFullContent) return false
|
||||
if (important != other.important) return false
|
||||
|
||||
|
@ -58,8 +61,9 @@ data class Feed(
|
|||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + groupId
|
||||
result = 31 * result + (groupId ?: 0)
|
||||
result = 31 * result + accountId
|
||||
result = 31 * result + isNotification.hashCode()
|
||||
result = 31 * result + isFullContent.hashCode()
|
||||
result = 31 * result + (important ?: 0)
|
||||
return result
|
||||
|
|
|
@ -13,7 +13,10 @@ interface FeedDao {
|
|||
suspend fun queryAll(accountId: Int): List<Feed>
|
||||
|
||||
@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
|
||||
suspend fun update(vararg feed: Feed)
|
||||
|
|
|
@ -14,6 +14,14 @@ interface GroupDao {
|
|||
)
|
||||
fun queryAllGroupWithFeed(accountId: Int): Flow<MutableList<GroupWithFeed>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM `group`
|
||||
WHERE accountId = :accountId
|
||||
"""
|
||||
)
|
||||
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
|
||||
|
||||
@Insert
|
||||
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.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.dataStore
|
||||
|
@ -21,7 +24,12 @@ class ArticleRepository @Inject constructor(
|
|||
private val context: Context,
|
||||
private val articleDao: ArticleDao,
|
||||
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>> {
|
||||
return groupDao.queryAllGroupWithFeed(
|
||||
|
@ -90,4 +98,11 @@ class ArticleRepository @Inject constructor(
|
|||
suspend fun findArticleById(id: Int): ArticleWithFeed? {
|
||||
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.content.ContextCompat.getSystemService
|
||||
import androidx.work.*
|
||||
import com.github.muhrifqii.parserss.ParseRSS
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
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.feed.Feed
|
||||
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.RssNetworkDataSource
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import net.dankito.readability4j.extended.Readability4JExtended
|
||||
import okhttp3.*
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -46,6 +45,38 @@ class RssRepository @Inject constructor(
|
|||
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||
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 {
|
||||
val readability4J: Readability4J = Readability4JExtended(link, content)
|
||||
val article = readability4J.parse()
|
||||
|
@ -146,6 +177,10 @@ class RssRepository @Inject constructor(
|
|||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
||||
?: return
|
||||
val feeds = feedDao.queryAll(accountId)
|
||||
val feedNotificationMap = mutableMapOf<Int, Boolean>()
|
||||
feeds.forEach { feed ->
|
||||
feedNotificationMap[feed.id ?: 0] = feed.isNotification
|
||||
}
|
||||
val preTime = System.currentTimeMillis()
|
||||
val chunked = feeds.chunked(6)
|
||||
chunked.forEachIndexed { index, item ->
|
||||
|
@ -199,34 +234,41 @@ class RssRepository @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
it.reversed().forEach { articleList ->
|
||||
it.forEach { articleList ->
|
||||
val ids = articleDao.insertList(articleList)
|
||||
articleList.forEachIndexed { index, article ->
|
||||
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||
.setContentTitle(article.title)
|
||||
.setContentText(article.shortDescription)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
ids[index].toInt(),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra(Symbol.EXTRA_ARTICLE_ID, ids[index].toInt())
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (feedNotificationMap[article.feedId] == true) {
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
||||
).setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||
.setContentTitle(article.title)
|
||||
.setContentText(article.shortDescription)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
ids[index].toInt(),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra(
|
||||
Symbol.EXTRA_ARTICLE_ID,
|
||||
ids[index].toInt()
|
||||
)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
notificationManager.notify(
|
||||
ids[index].toInt(),
|
||||
builder.build().apply {
|
||||
flags = Notification.FLAG_AUTO_CANCEL
|
||||
}
|
||||
)
|
||||
notificationManager.notify(ids[index].toInt(), builder.build().apply {
|
||||
flags = Notification.FLAG_AUTO_CANCEL
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}.buffer().onCompletion {
|
||||
|
@ -256,7 +298,6 @@ class RssRepository @Inject constructor(
|
|||
feed: Feed,
|
||||
latestTitle: String? = null,
|
||||
): List<Article> {
|
||||
ParseRSS.init(XmlPullParserFactory.newInstance())
|
||||
val a = mutableListOf<Article>()
|
||||
try {
|
||||
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package me.ash.reader.data.source
|
||||
|
||||
import com.github.muhrifqii.parserss.ParseRSS
|
||||
import com.github.muhrifqii.parserss.RSSFeedObject
|
||||
import com.github.muhrifqii.parserss.retrofit.ParseRSSConverterFactory
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
|
@ -15,6 +17,7 @@ interface RssNetworkDataSource {
|
|||
|
||||
fun getInstance(): RssNetworkDataSource {
|
||||
return instance ?: synchronized(this) {
|
||||
ParseRSS.init(XmlPullParserFactory.newInstance())
|
||||
instance ?: Retrofit.Builder()
|
||||
.baseUrl("https://api.feeddd.org/feeds/")
|
||||
.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.group.Group
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import me.ash.reader.ui.extension.animateScrollToPage
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
|
@ -62,10 +63,7 @@ class HomeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun scrollToPage(scope: CoroutineScope, targetPage: Int, callback: () -> Unit = {}) {
|
||||
scope.launch {
|
||||
_viewState.value.pagerState.animateScrollToPage(targetPage)
|
||||
callback()
|
||||
}
|
||||
_viewState.value.pagerState.animateScrollToPage(scope, targetPage, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +74,7 @@ data class FilterState(
|
|||
)
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
data class HomeViewState constructor(
|
||||
data class HomeViewState(
|
||||
val pagerState: PagerState = PagerState(1),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package me.ash.reader.ui.page.home.article
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
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.SmallTopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
@Composable
|
||||
fun ArticlePageTopBar(
|
||||
|
@ -16,10 +18,14 @@ fun ArticlePageTopBar(
|
|||
readAllOnClick: () -> Unit = {},
|
||||
searchOnClick: () -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = backOnClick) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
backOnClick()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowBackIosNew,
|
||||
contentDescription = "Back",
|
||||
|
@ -28,14 +34,20 @@ fun ArticlePageTopBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = readAllOnClick) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
readAllOnClick()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.DoneAll,
|
||||
contentDescription = "Done All",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
IconButton(onClick = searchOnClick) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
searchOnClick()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = "Search",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package me.ash.reader.ui.page.home.feed
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
|
@ -24,7 +23,6 @@ fun ColumnScope.FeedList(
|
|||
) {
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
feeds.forEach { feed ->
|
||||
Log.i("RLog", "FeedList: ${feed.icon}")
|
||||
FeedBar(
|
||||
barButtonType = ItemType(
|
||||
// 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.HomeViewModel
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
@ -31,6 +33,7 @@ fun FeedPage(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: FeedViewModel = hiltViewModel(),
|
||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
|
||||
filter: Filter,
|
||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
||||
) {
|
||||
|
@ -59,21 +62,6 @@ fun FeedPage(
|
|||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
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 = {
|
||||
viewModel.dispatch(FeedViewAction.AddFromFile(it))
|
||||
},
|
||||
|
@ -102,7 +90,7 @@ fun FeedPage(
|
|||
homeViewModel.dispatch(HomeViewAction.Sync())
|
||||
},
|
||||
subscribeOnClick = {
|
||||
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(true))
|
||||
subscribeViewModel.dispatch(SubscribeViewAction.Show)
|
||||
},
|
||||
)
|
||||
LazyColumn(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package me.ash.reader.ui.page.home.feed
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
|
@ -11,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
|
@ -22,10 +24,12 @@ fun FeedPageTopBar(
|
|||
syncOnClick: () -> Unit = {},
|
||||
subscribeOnClick: () -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
navController.navigate(route = RouteName.SETTINGS)
|
||||
}) {
|
||||
Icon(
|
||||
|
@ -38,6 +42,7 @@ fun FeedPageTopBar(
|
|||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
if (isSyncing) return@IconButton
|
||||
syncOnClick()
|
||||
}) {
|
||||
|
@ -48,7 +53,10 @@ fun FeedPageTopBar(
|
|||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = subscribeOnClick) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
subscribeOnClick()
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(26.dp),
|
||||
imageVector = Icons.Rounded.Add,
|
||||
|
|
|
@ -33,28 +33,6 @@ class FeedViewModel @Inject constructor(
|
|||
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
|
||||
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 listState: LazyListState = LazyListState(),
|
||||
val groupsVisible: Boolean = true,
|
||||
var subscribeDialogVisible: Boolean = false,
|
||||
var subscribeDialogFeedLink: String = "",
|
||||
)
|
||||
|
||||
sealed class FeedViewAction {
|
||||
|
@ -194,12 +170,4 @@ sealed class FeedViewAction {
|
|||
data class ScrollToItem(
|
||||
val index: Int
|
||||
) : 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
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Article
|
||||
|
@ -17,31 +19,56 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.google.accompanist.flowlayout.MainAxisAlignment
|
||||
import me.ash.reader.data.group.Group
|
||||
import me.ash.reader.ui.widget.SelectionChip
|
||||
|
||||
@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 {
|
||||
Link()
|
||||
Link(
|
||||
text = link
|
||||
)
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
|
||||
Preset()
|
||||
Preset(
|
||||
selectedNotificationPreset = selectedNotificationPreset,
|
||||
selectedFullContentParsePreset = selectedFullContentParsePreset,
|
||||
notificationPresetOnClick = notificationPresetOnClick,
|
||||
fullContentParsePresetOnClick = fullContentParsePresetOnClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
|
||||
AddToGroup()
|
||||
AddToGroup(
|
||||
groups = groups,
|
||||
selectedGroupId = selectedGroupId,
|
||||
groupOnClick = groupOnClick,
|
||||
onKeyboardAction = onKeyboardAction,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Link() {
|
||||
private fun Link(
|
||||
text: String,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = "https://material.io/feed.xml",
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
|
@ -49,7 +76,12 @@ private fun Link() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Preset() {
|
||||
private fun Preset(
|
||||
selectedNotificationPreset: Boolean = false,
|
||||
selectedFullContentParsePreset: Boolean = false,
|
||||
notificationPresetOnClick: () -> Unit = {},
|
||||
fullContentParsePresetOnClick: () -> Unit = {},
|
||||
) {
|
||||
Text(
|
||||
text = "预设",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
|
@ -62,7 +94,7 @@ private fun Preset() {
|
|||
mainAxisSpacing = 10.dp,
|
||||
) {
|
||||
SelectionChip(
|
||||
selected = true,
|
||||
selected = selectedNotificationPreset,
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Notifications,
|
||||
|
@ -70,7 +102,7 @@ private fun Preset() {
|
|||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
},
|
||||
onClick = { /*TODO*/ },
|
||||
onClick = notificationPresetOnClick,
|
||||
) {
|
||||
Text(
|
||||
text = "接收通知",
|
||||
|
@ -79,7 +111,7 @@ private fun Preset() {
|
|||
)
|
||||
}
|
||||
SelectionChip(
|
||||
selected = false,
|
||||
selected = selectedFullContentParsePreset,
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Article,
|
||||
|
@ -87,10 +119,10 @@ private fun Preset() {
|
|||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
},
|
||||
onClick = { /*TODO*/ }
|
||||
onClick = fullContentParsePresetOnClick,
|
||||
) {
|
||||
Text(
|
||||
text = "全文输出",
|
||||
text = "全文解析",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
|
@ -99,7 +131,12 @@ private fun Preset() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AddToGroup() {
|
||||
private fun AddToGroup(
|
||||
groups: List<Group>,
|
||||
selectedGroupId: Int,
|
||||
groupOnClick: (groupId: Int) -> Unit = {},
|
||||
onKeyboardAction: () -> Unit = {},
|
||||
) {
|
||||
Text(
|
||||
text = "添加到组",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
|
@ -111,49 +148,23 @@ private fun AddToGroup() {
|
|||
crossAxisSpacing = 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(
|
||||
selected = false,
|
||||
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(
|
||||
modifier = Modifier.width(56.dp),
|
||||
|
@ -165,6 +176,11 @@ private fun AddToGroup() {
|
|||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
),
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
onKeyboardAction()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.ContentPaste
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
@ -24,6 +28,7 @@ import kotlinx.coroutines.delay
|
|||
@Composable
|
||||
fun SearchViewPage(
|
||||
inputContent: String = "",
|
||||
errorMessage: String = "",
|
||||
onValueChange: (String) -> Unit = {},
|
||||
onKeyboardAction: () -> Unit = {},
|
||||
) {
|
||||
|
@ -34,42 +39,67 @@ fun SearchViewPage(
|
|||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
TextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
value = inputContent,
|
||||
onValueChange = {
|
||||
onValueChange(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "订阅源或站点链接",
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
TextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
value = inputContent,
|
||||
onValueChange = {
|
||||
onValueChange(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "订阅源或站点链接",
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
)
|
||||
},
|
||||
isError = errorMessage.isNotEmpty(),
|
||||
singleLine = true,
|
||||
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 = {
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ContentPaste,
|
||||
contentDescription = "Paste",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
onKeyboardAction()
|
||||
}
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
// focusRequester.requestFocus()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ContentPaste,
|
||||
contentDescription = "Paste",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
onKeyboardAction()
|
||||
}
|
||||
)
|
||||
)
|
||||
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.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 java.io.InputStream
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun SubscribeDialog(
|
||||
visible: Boolean,
|
||||
hiddenFunction: () -> Unit,
|
||||
inputContent: String = "",
|
||||
onValueChange: (String) -> Unit = {},
|
||||
onKeyboardAction: () -> Unit = {},
|
||||
viewModel: SubscribeViewModel = hiltViewModel(),
|
||||
openInputStreamCallback: (InputStream) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri ->
|
||||
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
||||
|
@ -31,49 +32,123 @@ 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(
|
||||
visible = visible,
|
||||
onDismissRequest = hiddenFunction,
|
||||
visible = viewState.visible,
|
||||
onDismissRequest = {
|
||||
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.RssFeed,
|
||||
contentDescription = "Subscribe",
|
||||
)
|
||||
},
|
||||
title = { Text("订阅") },
|
||||
title = {
|
||||
Text(
|
||||
when (viewState.pagerState.currentPage) {
|
||||
0 -> "订阅"
|
||||
else -> viewState.feed?.name ?: "未知"
|
||||
}
|
||||
)
|
||||
},
|
||||
text = {
|
||||
SubscribeViewPager(
|
||||
inputContent = inputContent,
|
||||
onValueChange = onValueChange,
|
||||
onKeyboardAction = onKeyboardAction,
|
||||
// height = when (viewState.pagerState.currentPage) {
|
||||
// 0 -> 84.dp
|
||||
// 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 = {
|
||||
TextButton(
|
||||
enabled = inputContent.isNotEmpty(),
|
||||
onClick = {
|
||||
hiddenFunction()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "搜索",
|
||||
color = if (inputContent.isNotEmpty()) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
when (viewState.pagerState.currentPage) {
|
||||
0 -> {
|
||||
TextButton(
|
||||
enabled = viewState.inputContent.isNotEmpty(),
|
||||
onClick = {
|
||||
viewModel.dispatch(SubscribeViewAction.Search(scope))
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "搜索",
|
||||
color = if (viewState.inputContent.isNotEmpty()) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
1 -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(SubscribeViewAction.Subscribe)
|
||||
}
|
||||
) {
|
||||
Text("订阅")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launcher.launch("*/*")
|
||||
hiddenFunction()
|
||||
when (viewState.pagerState.currentPage) {
|
||||
0 -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launcher.launch("*/*")
|
||||
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||
}
|
||||
) {
|
||||
Text("导入OPML文件")
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||
}
|
||||
) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("导入OPML文件")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.PagerState
|
||||
import me.ash.reader.data.group.Group
|
||||
import me.ash.reader.ui.widget.ViewPager
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun SubscribeViewPager(
|
||||
height: Dp = Dp.Unspecified,
|
||||
inputContent: String = "",
|
||||
errorMessage: String = "",
|
||||
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(
|
||||
modifier = Modifier.height(height),
|
||||
state = pagerState,
|
||||
userScrollEnabled = false,
|
||||
composableList = listOf(
|
||||
{
|
||||
SearchViewPage(
|
||||
inputContent = inputContent,
|
||||
errorMessage = errorMessage,
|
||||
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.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -74,33 +73,31 @@ fun ReadPage(
|
|||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
if (viewState.articleWithFeed == null) return@AnimatedVisibility
|
||||
SelectionContainer {
|
||||
LazyColumn(
|
||||
state = viewState.listState,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
val article = viewState.articleWithFeed.article
|
||||
val feed = viewState.articleWithFeed.feed
|
||||
LazyColumn(
|
||||
state = viewState.listState,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
val article = viewState.articleWithFeed.article
|
||||
val feed = viewState.articleWithFeed.feed
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.paddingFixedHorizontal()
|
||||
) {
|
||||
Header(context, article, feed)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
WebView(
|
||||
content = viewState.content ?: "",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.paddingFixedHorizontal()
|
||||
) {
|
||||
Header(context, article, feed)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
WebView(
|
||||
content = viewState.content ?: "",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package me.ash.reader.ui.page.home.read
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
|
@ -11,16 +12,21 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ReadPageTopBar(
|
||||
btnBackOnClickListener: () -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { btnBackOnClickListener() }) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
btnBackOnClickListener()
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(28.dp),
|
||||
imageVector = Icons.Rounded.Close,
|
||||
|
@ -30,19 +36,23 @@ fun ReadPageTopBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { /*TODO*/ }) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Rounded.Share,
|
||||
contentDescription = "Add",
|
||||
contentDescription = "Share",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { /*TODO*/ }) {
|
||||
IconButton(onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(28.dp),
|
||||
imageVector = Icons.Rounded.MoreHoriz,
|
||||
contentDescription = "Add",
|
||||
contentDescription = "More",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package me.ash.reader.ui.widget
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
|
@ -14,11 +13,12 @@ fun Dialog(
|
|||
confirmButton: @Composable () -> Unit,
|
||||
dismissButton: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
// AnimatedVisibility(
|
||||
// visible = visible,
|
||||
// enter = fadeIn() + expandVertically(),
|
||||
// exit = fadeOut() + shrinkVertically(),
|
||||
// ) {
|
||||
if (visible) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
icon = icon,
|
||||
|
|
|
@ -16,7 +16,7 @@ fun ViewPager(
|
|||
modifier: Modifier = Modifier,
|
||||
state: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
||||
composableList: List<@Composable () -> Unit>,
|
||||
userScrollEnabled: Boolean = true
|
||||
userScrollEnabled: Boolean = true,
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = composableList.size,
|
||||
|
|
Loading…
Reference in New Issue
Block a user