Compare commits

...

6 Commits

12 changed files with 111 additions and 115 deletions

View File

@ -11,7 +11,7 @@ on:
jobs:
build:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -20,8 +20,13 @@ jobs:
with:
distribution: 'temurin'
java-version: '17'
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Android Emulator Runner
uses: ReactiveCircus/android-emulator-runner@v2.28.0
uses: ReactiveCircus/android-emulator-runner@v2.30.1
with:
api-level: 29
script: ./gradlew clean build connectedCheck jacocoFullReport

View File

@ -79,6 +79,7 @@ class ItemScreen(
val snackbarHostState = remember { SnackbarHostState() }
var isScrollable by remember { mutableStateOf(true) }
var refreshAndroidView by remember { mutableStateOf(true) }
// https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#parent-compose-child-view
val bottomBarHeight = 64.dp
@ -140,7 +141,7 @@ class ItemScreen(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
ItemScreenBottomBar(
item = item,
state = state.bottomBarState,
accentColor = accentColor,
modifier = Modifier
.height(bottomBarHeight)
@ -198,16 +199,20 @@ class ItemScreen(
}
},
update = { nestedScrollView ->
val relativeLayout =
(nestedScrollView.children.toList()[0] as RelativeLayout)
val webView = relativeLayout.children.toList()[1] as ItemWebView
if (refreshAndroidView) {
val relativeLayout =
(nestedScrollView.children.toList()[0] as RelativeLayout)
val webView = relativeLayout.children.toList()[1] as ItemWebView
webView.loadText(
itemWithFeed = itemWithFeed,
accentColor = accentColor,
backgroundColor = backgroundColor,
onBackgroundColor = onBackgroundColor
)
webView.loadText(
itemWithFeed = itemWithFeed,
accentColor = accentColor,
backgroundColor = backgroundColor,
onBackgroundColor = onBackgroundColor
)
refreshAndroidView = false
}
}
)
}

View File

@ -13,14 +13,20 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
import com.readrops.app.compose.R
import com.readrops.app.compose.util.FeedColors
import com.readrops.app.compose.util.theme.spacing
import com.readrops.db.entities.Item
data class BottomBarState(
val isRead: Boolean = false,
val isStarred: Boolean = false
)
@Composable
fun ItemScreenBottomBar(
item: Item,
state: BottomBarState,
accentColor: Color,
onShare: () -> Unit,
onOpenUrl: () -> Unit,
@ -28,6 +34,11 @@ fun ItemScreenBottomBar(
onChangeStarState: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val tint = if (FeedColors.isColorDark(accentColor.toArgb()))
Color.White
else
Color.Black
Surface(
color = accentColor,
modifier = modifier.fillMaxWidth()
@ -37,27 +48,29 @@ fun ItemScreenBottomBar(
modifier = Modifier.padding(MaterialTheme.spacing.shortSpacing)
) {
IconButton(
onClick = { onChangeReadState(!item.isRead) }
onClick = { onChangeReadState(!state.isRead) }
) {
Icon(
painter = painterResource(
id = if (item.isRead)
id = if (state.isRead)
R.drawable.ic_remove_done
else R.drawable.ic_done_all
),
tint = tint,
contentDescription = null
)
}
IconButton(
onClick = { onChangeStarState(!item.isStarred) }
onClick = { onChangeStarState(!state.isStarred) }
) {
Icon(
painter = painterResource(
id = if (item.isStarred)
id = if (state.isStarred)
R.drawable.ic_star
else R.drawable.ic_star_outline
),
tint = tint,
contentDescription = null
)
}
@ -67,6 +80,7 @@ fun ItemScreenBottomBar(
) {
Icon(
imageVector = Icons.Default.Share,
tint = tint,
contentDescription = null
)
}
@ -76,6 +90,7 @@ fun ItemScreenBottomBar(
) {
Icon(
painter = painterResource(id = R.drawable.ic_open_in_browser),
tint = tint,
contentDescription = null
)
}

View File

@ -53,7 +53,13 @@ class ItemScreenModel(
database.newItemDao().selectItemById(query)
.collect { itemWithFeed ->
mutableState.update {
it.copy(itemWithFeed = itemWithFeed)
it.copy(
itemWithFeed = itemWithFeed,
bottomBarState = BottomBarState(
isRead = itemWithFeed.item.isRead,
isStarred = itemWithFeed.item.isStarred
)
)
}
}
}
@ -150,6 +156,7 @@ class ItemScreenModel(
@Stable
data class ItemState(
val itemWithFeed: ItemWithFeed? = null,
val bottomBarState: BottomBarState = BottomBarState(),
val imageDialogUrl: String? = null,
val fileDownloadedEvent: Boolean = false
)

View File

@ -4,7 +4,7 @@ import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.filters.MainFilter
import com.readrops.db.queries.FoldersAndFeedsQueriesBuilder
import com.readrops.db.queries.FeedUnreadItemsCountQueryBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -13,24 +13,24 @@ class GetFoldersWithFeeds(
) {
fun get(accountId: Int, mainFilter: MainFilter): Flow<Map<Folder?, List<Feed>>> {
val foldersAndFeedsQuery =
FoldersAndFeedsQueriesBuilder.buildFoldersAndFeedsQuery(accountId, mainFilter)
val feedsWithoutFolderQuery =
FoldersAndFeedsQueriesBuilder.buildFeedsWithoutFolderQuery(accountId, mainFilter)
val query = FeedUnreadItemsCountQueryBuilder.build(accountId, mainFilter)
return combine(
flow = database.newFolderDao()
.selectFoldersAndFeeds(foldersAndFeedsQuery),
flow2 = database.newFeedDao()
.selectFeedsWithoutFolder(feedsWithoutFolderQuery)
) { folders, feedsWithoutFolder ->
flow = database.newFolderDao().selectFoldersAndFeeds(accountId),
flow2 = database.newItemDao().selectFeedUnreadItemsCount(query)
) { folders, itemCounts ->
val foldersWithFeeds = folders.groupBy(
keySelector = {
Folder(
id = it.folderId,
name = it.folderName,
accountId = it.accountId
) as Folder?
if (it.folderId != null) {
Folder(
id = it.folderId!!,
name = it.folderName,
accountId = it.accountId
)
} else {
null
}
},
valueTransform = {
Feed(
@ -40,10 +40,11 @@ class GetFoldersWithFeeds(
url = it.feedUrl,
siteUrl = it.feedSiteUrl,
description = it.feedDescription,
unreadCount = it.unreadCount
unreadCount = itemCounts[it.feedId] ?: 0
)
}
).mapValues { listEntry ->
// Empty folder case
if (listEntry.value.any { it.id == 0 }) {
listOf()
} else {
@ -51,25 +52,7 @@ class GetFoldersWithFeeds(
}
}
if (feedsWithoutFolder.isNotEmpty()) {
foldersWithFeeds + mapOf(
Pair(
null,
feedsWithoutFolder.map { feedWithoutFolder ->
Feed(
id = feedWithoutFolder.feedId,
name = feedWithoutFolder.feedName,
iconUrl = feedWithoutFolder.feedIcon,
url = feedWithoutFolder.feedUrl,
siteUrl = feedWithoutFolder.feedSiteUrl,
description = feedWithoutFolder.feedDescription,
unreadCount = feedWithoutFolder.unreadCount
)
})
)
} else {
foldersWithFeeds
}
foldersWithFeeds
}
}
}

View File

@ -45,4 +45,7 @@ object FeedColors : KoinComponent {
val b = color shr 0 and 0xff
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
fun isColorDark(color: Int) = getColorLuma(color) < 130
}

View File

@ -9,7 +9,7 @@ object Utils {
private const val AVERAGE_WORDS_PER_MINUTE = 250
fun readTimeFromString(value: String): Double {
val nbWords = value.split("\\s+").size
val nbWords = value.split(Regex("\\s+")).size
return nbWords.toDouble() / AVERAGE_WORDS_PER_MINUTE
}

View File

@ -2,19 +2,19 @@ package com.readrops.db.dao.newdao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
import com.readrops.db.pojo.FolderWithFeed
import kotlinx.coroutines.flow.Flow
@Dao
interface NewFolderDao : NewBaseDao<Folder> {
@RawQuery(observedEntities = [Folder::class, Feed::class, Item::class])
fun selectFoldersAndFeeds(query: SupportSQLiteQuery): Flow<List<FolderWithFeed>>
@Query("Select Feed.id As feedId, Feed.name As feedName, Feed.icon_url As feedIcon, Feed.url As feedUrl, " +
"Feed.siteUrl As feedSiteUrl, Feed.description as feedDescription, Feed.account_id As accountId, " +
"Folder.id As folderId, Folder.name As folderName, 0 as unreadCount " +
" From Feed Left Join Folder On Folder.id = Feed.folder_id " +
"Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Feed.account_id = :accountId Group By Feed.id")
fun selectFoldersAndFeeds(accountId: Int): Flow<List<FolderWithFeed>>
@Query("Select * From Folder Where account_id = :accountId")
fun selectFolders(accountId: Int): Flow<List<Folder>>

View File

@ -2,6 +2,7 @@ package com.readrops.db.dao.newdao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
@ -49,4 +50,8 @@ abstract class NewItemDao : NewBaseDao<Item> {
@Query("Select count(*) From Item Inner Join Feed On Item.feed_id = Feed.id Where read = 0 and account_id = :accountId " +
"And DateTime(Round(Item.pub_date / 1000), 'unixepoch') Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\")")
abstract fun selectUnreadNewItemsCount(accountId: Int): Flow<Int>
@RawQuery(observedEntities = [Item::class])
abstract fun selectFeedUnreadItemsCount(query: SupportSQLiteQuery):
Flow<Map<@MapColumn(columnName = "feed_id") Int, @MapColumn(columnName = "item_count") Int>>
}

View File

@ -13,15 +13,14 @@ data class FeedWithFolder(
) : Parcelable
data class FolderWithFeed(
val folderId: Int,
val folderName: String,
val folderId: Int?,
val folderName: String?,
val feedId: Int = 0,
val feedName: String? = null,
val feedIcon: String? = null,
val feedUrl: String? = null,
val feedDescription: String? = null,
val feedSiteUrl: String? = null,
val unreadCount: Int = 0,
val accountId: Int = 0
)

View File

@ -0,0 +1,25 @@
package com.readrops.db.queries
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.readrops.db.filters.MainFilter
import org.intellij.lang.annotations.Language
object FeedUnreadItemsCountQueryBuilder {
fun build(accountId: Int, mainFilter: MainFilter): SupportSQLiteQuery {
val filter = when (mainFilter) {
MainFilter.STARS -> "And Item.starred = 1"
MainFilter.NEW -> "And DateTime(Round(Item.pub_date / 1000), 'unixepoch') Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\") "
else -> ""
}
@Language("SQL")
val query = SimpleSQLiteQuery("""
Select feed_id, count(*) AS item_count From Item Inner Join Feed On Feed.id = Item.feed_id
Where read = 0 And account_id = $accountId $filter Group By feed_id
""".trimIndent())
return query
}
}

View File

@ -1,51 +0,0 @@
package com.readrops.db.queries
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.readrops.db.filters.MainFilter
import org.intellij.lang.annotations.Language
object FoldersAndFeedsQueriesBuilder {
fun buildFoldersAndFeedsQuery(accountId: Int, mainFilter: MainFilter): SupportSQLiteQuery {
val filter = when (mainFilter) {
MainFilter.STARS -> "And Item.starred = 1"
MainFilter.NEW -> "And DateTime(Round(Item.pub_date / 1000), 'unixepoch') Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\") "
else -> ""
}
@Language("SQL")
val query = SimpleSQLiteQuery("""
With main As (Select Folder.id As folderId, Folder.name As folderName, Feed.id As feedId,
Feed.name As feedName, Feed.icon_url As feedIcon, Feed.url As feedUrl, Feed.siteUrl As feedSiteUrl, Feed.description as feedDescription,
Folder.account_id As accountId, Item.read as itemRead
From Folder Left Join Feed On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id
Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Feed.account_id = $accountId $filter)
Select folderId, folderName, feedId, feedName, feedIcon, feedUrl, feedSiteUrl, accountId, feedDescription,
(Select count(*) From main Where (itemRead = 0)) as unreadCount
From main Group by feedId, folderId Order By folderName, feedName
""".trimIndent())
return query
}
fun buildFeedsWithoutFolderQuery(accountId: Int, mainFilter: MainFilter): SupportSQLiteQuery {
val filter = when (mainFilter) {
MainFilter.STARS -> "And Item.starred = 1 "
MainFilter.NEW -> "And DateTime(Round(Item.pub_date / 1000), 'unixepoch') Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\") "
else -> ""
}
@Language("SQL")
val query = SimpleSQLiteQuery("""
With main As (Select Feed.id As feedId, Feed.name As feedName, Feed.icon_url As feedIcon, feed.description as feedDescription,
Feed.url As feedUrl, Feed.siteUrl As feedSiteUrl, Feed.account_id As accountId, Item.read As itemRead
From Feed Left Join Item On Feed.id = Item.feed_id Where Feed.folder_id is Null And Feed.account_id = $accountId $filter)
Select feedId, feedName, feedIcon, feedUrl, feedSiteUrl, accountId, feedDescription,
(Select count(*) From main Where (itemRead = 0)) as unreadCount From main Group by feedId Order By feedName
""".trimIndent())
return query
}
}