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.LocalDateTime
|
||||||
import org.joda.time.format.DateTimeFormat
|
import org.joda.time.format.DateTimeFormat
|
||||||
import org.joda.time.format.DateTimeFormatterBuilder
|
import org.joda.time.format.DateTimeFormatterBuilder
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
object DateUtils {
|
object DateUtils {
|
||||||
|
|
||||||
@ -35,16 +35,16 @@ object DateUtils {
|
|||||||
null
|
null
|
||||||
} else try {
|
} else try {
|
||||||
val formatter = DateTimeFormatterBuilder()
|
val formatter = DateTimeFormatterBuilder()
|
||||||
.appendOptional(DateTimeFormat.forPattern("$RSS_2_BASE_PATTERN ").parser) // with timezone
|
.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(RSS_2_BASE_PATTERN).parser) // no timezone, important order here
|
||||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).parser)
|
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).parser)
|
||||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).parser)
|
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).parser)
|
||||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).parser)
|
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).parser)
|
||||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).parser)
|
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).parser)
|
||||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).parser)
|
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).parser)
|
||||||
.toFormatter()
|
.toFormatter()
|
||||||
.withLocale(Locale.ENGLISH)
|
.withLocale(Locale.ENGLISH)
|
||||||
.withOffsetParsed()
|
.withOffsetParsed()
|
||||||
|
|
||||||
formatter.parseLocalDateTime(value)
|
formatter.parseLocalDateTime(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -54,14 +54,26 @@ object DateUtils {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun formattedDateByLocal(dateTime: LocalDateTime): String {
|
fun formattedDateByLocal(dateTime: LocalDateTime): String {
|
||||||
return DateTimeFormat.mediumDate()
|
return DateTimeFormat.mediumDate()
|
||||||
.withLocale(Locale.getDefault())
|
.withLocale(Locale.getDefault())
|
||||||
.print(dateTime)
|
.print(dateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun formattedDateTimeByLocal(dateTime: LocalDateTime): String {
|
fun formattedDateTimeByLocal(dateTime: LocalDateTime): String {
|
||||||
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
||||||
.withLocale(Locale.getDefault())
|
.withLocale(Locale.getDefault())
|
||||||
.print(dateTime)
|
.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.AccountScreenModel
|
||||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
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.BaseRepository
|
||||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||||
@ -20,6 +21,10 @@ val composeAppModule = module {
|
|||||||
|
|
||||||
factory { AccountScreenModel(get()) }
|
factory { AccountScreenModel(get()) }
|
||||||
|
|
||||||
|
factory { (itemId: Int) ->
|
||||||
|
ItemScreenModel(get(), itemId)
|
||||||
|
}
|
||||||
|
|
||||||
single { GetFoldersWithFeeds(get()) }
|
single { GetFoldersWithFeeds(get()) }
|
||||||
|
|
||||||
// repositories
|
// repositories
|
||||||
|
@ -1,13 +1,194 @@
|
|||||||
package com.readrops.app.compose.item
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.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
|
@Composable
|
||||||
override fun Content() {
|
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(
|
IconText(
|
||||||
icon = painterResource(id = R.drawable.ic_hourglass_empty),
|
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
|
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])
|
@RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class])
|
||||||
abstract fun selectAll(query: SupportSQLiteQuery): PagingSource<Int, ItemWithFeed>
|
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")
|
@Query("Update Item Set read = :read Where id = :itemId")
|
||||||
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
||||||
|
|
||||||
|
@ -5,16 +5,21 @@ import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
|||||||
|
|
||||||
object ItemSelectionQueryBuilder {
|
object ItemSelectionQueryBuilder {
|
||||||
|
|
||||||
private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "Item.description", "content",
|
private val COLUMNS = arrayOf(
|
||||||
"link", "pub_date", "image_link", "author", "Item.read", "text_color",
|
"Item.id", "Item.remoteId", "title", "Item.description", "content",
|
||||||
"background_color", "read_time", "Feed.name", "Feed.id as feedId", "siteUrl",
|
"link", "pub_date", "image_link", "author", "Item.read", "text_color", "icon_url",
|
||||||
"Folder.id as folder_id", "Folder.name as folder_name")
|
"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
|
* @param separateState Indicates if item state must be retrieved from ItemState table
|
||||||
@ -22,7 +27,8 @@ object ItemSelectionQueryBuilder {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun buildQuery(itemId: Int, separateState: Boolean): SupportSQLiteQuery {
|
fun buildQuery(itemId: Int, separateState: Boolean): SupportSQLiteQuery {
|
||||||
val tables = if (separateState) JOIN + SEPARATE_STATE_JOIN else JOIN
|
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 {
|
return SupportSQLiteQueryBuilder.builder(tables).run {
|
||||||
columns(columns)
|
columns(columns)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user