mirror of https://github.com/readrops/Readrops.git
Compare commits
6 Commits
8566b55e3f
...
9f87077945
Author | SHA1 | Date |
---|---|---|
Shinokuni | 9f87077945 | |
Shinokuni | a3ffde0d73 | |
Shinokuni | 6893e9a199 | |
Shinokuni | c55a9dc5e4 | |
Shinokuni | 45dc199ea2 | |
Shinokuni | 841b56e7e5 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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>>
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue