Add initial ItemScreen UI

This commit is contained in:
Shinokuni 2024-04-07 17:32:53 +02:00
parent 0ccb4aa9c8
commit 16e70519e4
9 changed files with 278 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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