mirror of
https://github.com/readrops/Readrops.git
synced 2025-02-03 03:57:36 +01:00
Add initial ItemScreen UI
This commit is contained in:
parent
0ccb4aa9c8
commit
16e70519e4
@ -3,7 +3,7 @@ package com.readrops.api.utils
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
object DateUtils {
|
||||
|
||||
@ -35,16 +35,16 @@ object DateUtils {
|
||||
null
|
||||
} else try {
|
||||
val formatter = DateTimeFormatterBuilder()
|
||||
.appendOptional(DateTimeFormat.forPattern("$RSS_2_BASE_PATTERN ").parser) // with timezone
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).parser) // no timezone, important order here
|
||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).parser)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.ENGLISH)
|
||||
.withOffsetParsed()
|
||||
.appendOptional(DateTimeFormat.forPattern("$RSS_2_BASE_PATTERN ").parser) // with timezone
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).parser) // no timezone, important order here
|
||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).parser)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.ENGLISH)
|
||||
.withOffsetParsed()
|
||||
|
||||
formatter.parseLocalDateTime(value)
|
||||
} catch (e: Exception) {
|
||||
@ -54,14 +54,26 @@ object DateUtils {
|
||||
@JvmStatic
|
||||
fun formattedDateByLocal(dateTime: LocalDateTime): String {
|
||||
return DateTimeFormat.mediumDate()
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun formattedDateTimeByLocal(dateTime: LocalDateTime): String {
|
||||
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
}
|
||||
|
||||
fun formattedDate(dateTime: LocalDateTime): String {
|
||||
val pattern = if (dateTime.year != LocalDateTime.now().year) {
|
||||
"dd MMMM yyyy"
|
||||
} else {
|
||||
"dd MMMM"
|
||||
}
|
||||
|
||||
return DateTimeFormat.forPattern(pattern)
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.readrops.app.compose
|
||||
import com.readrops.app.compose.account.AccountScreenModel
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
||||
import com.readrops.app.compose.item.ItemScreenModel
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||
@ -20,6 +21,10 @@ val composeAppModule = module {
|
||||
|
||||
factory { AccountScreenModel(get()) }
|
||||
|
||||
factory { (itemId: Int) ->
|
||||
ItemScreenModel(get(), itemId)
|
||||
}
|
||||
|
||||
single { GetFoldersWithFeeds(get()) }
|
||||
|
||||
// repositories
|
||||
|
@ -1,13 +1,194 @@
|
||||
package com.readrops.app.compose.item
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.util.components.AndroidScreen
|
||||
import com.readrops.app.compose.util.components.CenteredProgressIndicator
|
||||
import com.readrops.app.compose.util.components.IconText
|
||||
import com.readrops.app.compose.util.theme.ShortSpacer
|
||||
import com.readrops.app.compose.util.theme.VeryShortSpacer
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import com.readrops.db.pojo.ItemWithFeed
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ItemScreen : AndroidScreen() {
|
||||
class ItemScreen(
|
||||
private val itemId: Int,
|
||||
) : AndroidScreen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Text(text ="item screen")
|
||||
val screenModel =
|
||||
getScreenModel<ItemScreenModel>(parameters = { parametersOf(itemId) })
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val state by screenModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
if (state.itemWithFeed != null) {
|
||||
val itemWithFeed = state.itemWithFeed!!
|
||||
|
||||
if (itemWithFeed.item.imageLink != null) {
|
||||
BackgroundTitle(
|
||||
itemWithFeed = itemWithFeed
|
||||
)
|
||||
} else {
|
||||
val tintColor = if (itemWithFeed.bgColor != 0) {
|
||||
Color(itemWithFeed.bgColor)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
}
|
||||
|
||||
SimpleTitle(
|
||||
itemWithFeed = itemWithFeed,
|
||||
titleColor = tintColor,
|
||||
tintColor = tintColor,
|
||||
baseColor = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CenteredProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackgroundTitle(
|
||||
itemWithFeed: ItemWithFeed,
|
||||
) {
|
||||
val onScrimColor = Color.White.copy(alpha = 0.85f)
|
||||
val tintColor = if (itemWithFeed.bgColor != 0) {
|
||||
Color(itemWithFeed.bgColor)
|
||||
} else {
|
||||
onScrimColor
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 24.dp,
|
||||
bottomEnd = 24.dp
|
||||
),
|
||||
modifier = Modifier.height(IntrinsicSize.Max)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = itemWithFeed.item.imageLink,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
error = painterResource(id = R.drawable.ic_broken_image),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
)
|
||||
|
||||
Surface(
|
||||
color = Color.Black.copy(alpha = 0.7f),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
SimpleTitle(
|
||||
itemWithFeed = itemWithFeed,
|
||||
titleColor = onScrimColor,
|
||||
tintColor = tintColor,
|
||||
baseColor = onScrimColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleTitle(
|
||||
itemWithFeed: ItemWithFeed,
|
||||
titleColor: Color,
|
||||
tintColor: Color,
|
||||
baseColor: Color
|
||||
) {
|
||||
val item = itemWithFeed.item
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = itemWithFeed.feedIconUrl,
|
||||
contentDescription = itemWithFeed.feedName,
|
||||
placeholder = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
|
||||
VeryShortSpacer()
|
||||
|
||||
Text(
|
||||
text = itemWithFeed.feedName,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = baseColor
|
||||
)
|
||||
|
||||
ShortSpacer()
|
||||
|
||||
Text(
|
||||
text = item.title!!,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = titleColor,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
if (item.author != null) {
|
||||
ShortSpacer()
|
||||
|
||||
IconText(
|
||||
icon = painterResource(id = R.drawable.ic_person),
|
||||
text = itemWithFeed.item.author!!,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = baseColor,
|
||||
tint = tintColor
|
||||
)
|
||||
}
|
||||
|
||||
ShortSpacer()
|
||||
|
||||
val readTime =
|
||||
if (item.readTime < 1) "< 1 min" else "${item.readTime.roundToInt()} mins"
|
||||
Text(
|
||||
text = "${DateUtils.formattedDate(item.pubDate!!)} · $readTime",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = baseColor
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.readrops.app.compose.item
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.pojo.ItemWithFeed
|
||||
import com.readrops.db.queries.ItemSelectionQueryBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ItemScreenModel(
|
||||
private val database: Database,
|
||||
private val itemId: Int
|
||||
) : StateScreenModel<ItemState>(ItemState()) {
|
||||
|
||||
init {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
mutableState.update {
|
||||
val query = ItemSelectionQueryBuilder.buildQuery(itemId, false)
|
||||
|
||||
it.copy(
|
||||
itemWithFeed = database.newItemDao().selectItemById(query)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class ItemState(
|
||||
val itemWithFeed: ItemWithFeed? = null
|
||||
)
|
@ -135,7 +135,7 @@ fun TimelineItem(
|
||||
|
||||
IconText(
|
||||
icon = painterResource(id = R.drawable.ic_hourglass_empty),
|
||||
text = if (itemWithFeed.item.readTime < 1) "> 1 min" else "${itemWithFeed.item.readTime.roundToInt()} mins",
|
||||
text = if (itemWithFeed.item.readTime < 1) "< 1 min" else "${itemWithFeed.item.readTime.roundToInt()} mins",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
|
5
appcompose/src/main/res/drawable/ic_broken_image.xml
Normal file
5
appcompose/src/main/res/drawable/ic_broken_image.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
|
||||
|
||||
</vector>
|
5
appcompose/src/main/res/drawable/ic_person.xml
Normal file
5
appcompose/src/main/res/drawable/ic_person.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
|
||||
</vector>
|
@ -17,6 +17,9 @@ abstract class NewItemDao : NewBaseDao<Item> {
|
||||
@RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class])
|
||||
abstract fun selectAll(query: SupportSQLiteQuery): PagingSource<Int, ItemWithFeed>
|
||||
|
||||
@RawQuery(observedEntities = [Item::class, ItemState::class])
|
||||
abstract suspend fun selectItemById(query: SupportSQLiteQuery): ItemWithFeed
|
||||
|
||||
@Query("Update Item Set read = :read Where id = :itemId")
|
||||
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
||||
|
||||
|
@ -5,16 +5,21 @@ import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
|
||||
object ItemSelectionQueryBuilder {
|
||||
|
||||
private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "Item.description", "content",
|
||||
"link", "pub_date", "image_link", "author", "Item.read", "text_color",
|
||||
"background_color", "read_time", "Feed.name", "Feed.id as feedId", "siteUrl",
|
||||
"Folder.id as folder_id", "Folder.name as folder_name")
|
||||
private val COLUMNS = arrayOf(
|
||||
"Item.id", "Item.remoteId", "title", "Item.description", "content",
|
||||
"link", "pub_date", "image_link", "author", "Item.read", "text_color", "icon_url",
|
||||
"background_color", "read_time", "Feed.name", "Feed.id as feedId", "siteUrl",
|
||||
"Folder.id as folder_id", "Folder.name as folder_name"
|
||||
)
|
||||
|
||||
private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.starred = 1 Then 1 else 0 End starred")
|
||||
private val SEPARATE_STATE_COLUMNS =
|
||||
arrayOf("case When ItemState.starred = 1 Then 1 else 0 End starred")
|
||||
|
||||
private const val JOIN = "Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id"
|
||||
private const val JOIN =
|
||||
"Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id"
|
||||
|
||||
private const val SEPARATE_STATE_JOIN = " Left Join ItemState On ItemState.remote_id = Item.remoteId"
|
||||
private const val SEPARATE_STATE_JOIN =
|
||||
" Left Join ItemState On ItemState.remote_id = Item.remoteId"
|
||||
|
||||
/**
|
||||
* @param separateState Indicates if item state must be retrieved from ItemState table
|
||||
@ -22,7 +27,8 @@ object ItemSelectionQueryBuilder {
|
||||
@JvmStatic
|
||||
fun buildQuery(itemId: Int, separateState: Boolean): SupportSQLiteQuery {
|
||||
val tables = if (separateState) JOIN + SEPARATE_STATE_JOIN else JOIN
|
||||
val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus("starred")
|
||||
val columns =
|
||||
if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus("starred")
|
||||
|
||||
return SupportSQLiteQueryBuilder.builder(tables).run {
|
||||
columns(columns)
|
||||
|
Loading…
x
Reference in New Issue
Block a user