diff --git a/app/src/main/java/me/ash/reader/StringExt.kt b/app/src/main/java/me/ash/reader/StringExt.kt new file mode 100644 index 0000000..9a67a60 --- /dev/null +++ b/app/src/main/java/me/ash/reader/StringExt.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt index 5f697aa..481cda2 100644 --- a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt @@ -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 diff --git a/app/src/main/java/me/ash/reader/data/constant/Symbol.kt b/app/src/main/java/me/ash/reader/data/constant/Symbol.kt index 692f834..e76aa4c 100644 --- a/app/src/main/java/me/ash/reader/data/constant/Symbol.kt +++ b/app/src/main/java/me/ash/reader/data/constant/Symbol.kt @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/feed/Feed.kt b/app/src/main/java/me/ash/reader/data/feed/Feed.kt index a004e07..79663d9 100644 --- a/app/src/main/java/me/ash/reader/data/feed/Feed.kt +++ b/app/src/main/java/me/ash/reader/data/feed/Feed.kt @@ -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 diff --git a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt index 4d60d79..c3e6ac0 100644 --- a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt +++ b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt @@ -13,7 +13,10 @@ interface FeedDao { suspend fun queryAll(accountId: Int): List @Insert - suspend fun insertList(feed: List): List + suspend fun insert(feed: Feed): Long + + @Insert + suspend fun insertList(feeds: List): List @Update suspend fun update(vararg feed: Feed) diff --git a/app/src/main/java/me/ash/reader/data/group/GroupDao.kt b/app/src/main/java/me/ash/reader/data/group/GroupDao.kt index 991ccee..1a7443e 100644 --- a/app/src/main/java/me/ash/reader/data/group/GroupDao.kt +++ b/app/src/main/java/me/ash/reader/data/group/GroupDao.kt @@ -14,6 +14,14 @@ interface GroupDao { ) fun queryAllGroupWithFeed(accountId: Int): Flow> + @Query( + """ + SELECT * FROM `group` + WHERE accountId = :accountId + """ + ) + fun queryAllGroup(accountId: Int): Flow> + @Insert suspend fun insert(group: Group): Long diff --git a/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt b/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt index 1a004d2..d97b807 100644 --- a/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt @@ -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> { + val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 + return groupDao.queryAllGroup(accountId) + } fun pullFeeds(): Flow> { 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
) { + val feedId = feedDao.insert(feed).toInt() + articleDao.insertList(articles.map { + it.copy(feedId = feedId) + }) + } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt index ba3cae3..e878ecc 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt @@ -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
() + 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() + 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
{ - ParseRSS.init(XmlPullParserFactory.newInstance()) val a = mutableListOf
() try { val parseRss = rssNetworkDataSource.parseRss(feed.url) diff --git a/app/src/main/java/me/ash/reader/data/source/RssNetworkDataSource.kt b/app/src/main/java/me/ash/reader/data/source/RssNetworkDataSource.kt index 600139b..94189d8 100644 --- a/app/src/main/java/me/ash/reader/data/source/RssNetworkDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/RssNetworkDataSource.kt @@ -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()) diff --git a/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt b/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt new file mode 100644 index 0000000..5d9b4fb --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 7a71286..11f2056 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -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), ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt index 851fed7..05de02b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt @@ -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", diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedList.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedList.kt index 79a8088..9dac83e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedList.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedList.kt @@ -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 ?: "", diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPage.kt index 02f3856..1e4da3c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPage.kt @@ -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( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPageTopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPageTopBar.kt index 7594e72..6107c52 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPageTopBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedPageTopBar.kt @@ -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, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt index 49a89ed..9b56eb5 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt @@ -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 = 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() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt index c59d903..2628a2c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt @@ -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 = 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, + 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() + } + ) ) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SearchViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SearchViewPage.kt index 42d2f8c..0a0921c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SearchViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SearchViewPage.kt @@ -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)) + } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt index f68c077..e16d390 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt @@ -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文件") } }, ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt new file mode 100644 index 0000000..1b9b2d2 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt @@ -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 = _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
= emptyList(), + val notificationPreset: Boolean = false, + val fullContentParsePreset: Boolean = false, + val selectedGroupId: Int? = null, + val groups: Flow> = 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() +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt index 7e0f314..0d137f3 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt @@ -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 = 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, + ) } ) ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index 0c5e1eb..096662a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -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)) + } } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt index abfd134..164cced 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt @@ -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, ) } diff --git a/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt b/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt index 2939ac2..6660e39 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt @@ -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, diff --git a/app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt b/app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt index a42bcc0..c825a76 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt @@ -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,