Add subscribe a single feed feature

This commit is contained in:
Ash 2022-03-08 01:11:01 +08:00
parent 11ca1f1ae8
commit 1713331125
25 changed files with 697 additions and 249 deletions

View 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)
}

View File

@ -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

View File

@ -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"
} }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)
})
}
} }

View File

@ -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,34 +234,41 @@ 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}")
val builder = NotificationCompat.Builder( if (feedNotificationMap[article.feedId] == true) {
context, val builder = NotificationCompat.Builder(
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE context,
) 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)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent( .setContentIntent(
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
ids[index].toInt(), ids[index].toInt(),
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,
PendingIntent.FLAG_UPDATE_CURRENT 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 { }.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)

View File

@ -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>())

View File

@ -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()
}
}

View File

@ -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),
) )

View File

@ -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",

View File

@ -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 ?: "",

View File

@ -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(

View File

@ -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,

View File

@ -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()
} }

View File

@ -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()
}
)
) )
} }
} }

View File

@ -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,42 +39,67 @@ fun SearchViewPage(
focusRequester.requestFocus() focusRequester.requestFocus()
} }
Spacer(modifier = Modifier.height(10.dp)) Column {
TextField( Spacer(modifier = Modifier.height(10.dp))
modifier = Modifier.focusRequester(focusRequester), TextField(
colors = TextFieldDefaults.textFieldColors( modifier = Modifier.focusRequester(focusRequester),
backgroundColor = Color.Transparent, colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colorScheme.onSurface, backgroundColor = Color.Transparent,
textColor = MaterialTheme.colorScheme.onSurface, cursorColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary, textColor = MaterialTheme.colorScheme.onSurface,
), focusedIndicatorColor = MaterialTheme.colorScheme.primary,
value = inputContent, ),
onValueChange = { value = inputContent,
onValueChange(it) onValueChange = {
}, onValueChange(it)
placeholder = { },
Text( placeholder = {
text = "订阅源或站点链接", Text(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) 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, if (errorMessage.isNotEmpty()) {
trailingIcon = { SelectionContainer {
IconButton(onClick = { Text(
// focusRequester.requestFocus() text = errorMessage,
}) { color = MaterialTheme.colorScheme.error,
Icon( modifier = Modifier.padding(start = 16.dp),
imageVector = Icons.Rounded.ContentPaste, maxLines = 1,
contentDescription = "Paste", softWrap = false,
tint = MaterialTheme.colorScheme.primary
) )
} }
}, }
keyboardActions = KeyboardActions( Spacer(modifier = Modifier.height(10.dp))
onDone = { }
onKeyboardAction()
}
)
)
Spacer(modifier = Modifier.height(10.dp))
} }

View File

@ -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,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( 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 = {
TextButton( when (viewState.pagerState.currentPage) {
enabled = inputContent.isNotEmpty(), 0 -> {
onClick = { TextButton(
hiddenFunction() enabled = viewState.inputContent.isNotEmpty(),
} onClick = {
) { viewModel.dispatch(SubscribeViewAction.Search(scope))
Text( }
text = "搜索", ) {
color = if (inputContent.isNotEmpty()) { Text(
Color.Unspecified text = "搜索",
} else { color = if (viewState.inputContent.isNotEmpty()) {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
}
)
} }
) }
1 -> {
TextButton(
onClick = {
viewModel.dispatch(SubscribeViewAction.Subscribe)
}
) {
Text("订阅")
}
}
} }
}, },
dismissButton = { dismissButton = {
TextButton( when (viewState.pagerState.currentPage) {
onClick = { 0 -> {
launcher.launch("*/*") TextButton(
hiddenFunction() onClick = {
launcher.launch("*/*")
viewModel.dispatch(SubscribeViewAction.Hide)
}
) {
Text("导入OPML文件")
}
}
1 -> {
TextButton(
onClick = {
viewModel.dispatch(SubscribeViewAction.Hide)
}
) {
Text("取消")
}
} }
) {
Text("导入OPML文件")
} }
}, },
) )

View File

@ -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()
}

View File

@ -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,
)
} }
) )
) )

View File

@ -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,33 +73,31 @@ 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 .weight(1f),
.weight(1f), ) {
) { val article = viewState.articleWithFeed.article
val article = viewState.articleWithFeed.article val feed = viewState.articleWithFeed.feed
val feed = viewState.articleWithFeed.feed
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.paddingFixedHorizontal() .paddingFixedHorizontal()
) { ) {
Header(context, article, feed) 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(40.dp))
WebView(
content = viewState.content ?: "",
)
Spacer(modifier = Modifier.height(50.dp))
}
} }
} }
} }

View File

@ -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,
) )
} }

View File

@ -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,

View File

@ -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,