Merge branch 'develop' into fix-freshrss-get-link
This commit is contained in:
commit
9c73738cab
@ -54,8 +54,8 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
|
||||
testImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version"
|
||||
testImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
implementation(libs.bundles.koin)
|
||||
testImplementation(libs.bundles.kointest)
|
||||
|
||||
implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0'
|
||||
implementation 'org.redundent:kotlin-xml-builder:1.7.3'
|
||||
|
@ -4,13 +4,14 @@ import com.readrops.api.utils.exceptions.HttpException
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class ErrorInterceptor() : Interceptor {
|
||||
class ErrorInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
// TODO cover all cases
|
||||
if (!response.isSuccessful && response.code != 304) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
|
@ -87,8 +87,12 @@ dependencies {
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
|
||||
implementation(libs.bundles.koin)
|
||||
testImplementation(libs.bundles.kointest)
|
||||
/*
|
||||
testImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
testImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version"
|
||||
*/
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
@ -113,20 +117,5 @@ dependencies {
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.96.1'
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2022.12.00')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
|
||||
implementation 'androidx.activity:activity-compose:1.5.1'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
|
||||
def voyager = "1.0.0-rc03"
|
||||
implementation "cafe.adriel.voyager:voyager-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-androidx:$voyager"
|
||||
//implementation "cafe.adriel.voyager:voyager-koin:$voyager"
|
||||
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:1.3.3"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:1.3.3"
|
||||
implementation(libs.bundles.room)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.filters.FilterType;
|
||||
import com.readrops.db.filters.MainFilter;
|
||||
import com.readrops.db.filters.ListSortType;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
|
||||
@ -273,18 +273,18 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
switch (id) {
|
||||
default:
|
||||
case DrawerManager.ARTICLES_ITEM_ID:
|
||||
viewModel.setFilterType(FilterType.NO_FILTER);
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
scrollToTop = true;
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.articles);
|
||||
break;
|
||||
case DrawerManager.READ_LATER_ID:
|
||||
viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
|
||||
//viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.read_later);
|
||||
break;
|
||||
case DrawerManager.STARS_ID:
|
||||
viewModel.setFilterType(FilterType.STARS_FILTER);
|
||||
viewModel.setFilterType(MainFilter.STARS);
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.favorites);
|
||||
break;
|
||||
@ -302,14 +302,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
drawer.closeDrawer();
|
||||
|
||||
viewModel.setFilterFeedId((int) drawerItem.getIdentifier());
|
||||
viewModel.setFilterType(FilterType.FEED_FILTER);
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
viewModel.invalidate();
|
||||
setTitle(((SecondaryDrawerItem) drawerItem).getName().getText());
|
||||
} else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) {
|
||||
drawer.closeDrawer();
|
||||
|
||||
viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000));
|
||||
viewModel.setFilterType(FilterType.FOLDER_FILER);
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
viewModel.invalidate();
|
||||
setTitle(((CustomExpandableBadgeDrawerItem) drawerItem).getName().getText());
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.filters.FilterType;
|
||||
import com.readrops.db.filters.MainFilter;
|
||||
import com.readrops.db.filters.ListSortType;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
import com.readrops.db.queries.ItemsQueryBuilder;
|
||||
@ -54,8 +54,8 @@ public class MainViewModel extends ViewModel {
|
||||
itemsWithFeed = new MediatorLiveData<>();
|
||||
|
||||
queryFilters = new QueryFilters();
|
||||
queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean(
|
||||
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));
|
||||
/* queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean(
|
||||
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));*/
|
||||
}
|
||||
|
||||
//region main query
|
||||
@ -71,7 +71,7 @@ public class MainViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
DataSource.Factory<Integer, ItemWithFeed> items;
|
||||
items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters, currentAccount.getConfig().getUseSeparateState()));
|
||||
items = database.itemDao().selectAll(ItemsQueryBuilder.INSTANCE.buildItemsQuery(queryFilters, currentAccount.getConfig().getUseSeparateState()));
|
||||
|
||||
lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items),
|
||||
new PagedList.Config.Builder()
|
||||
@ -89,23 +89,23 @@ public class MainViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void setShowReadItems(boolean showReadItems) {
|
||||
queryFilters.setShowReadItems(showReadItems);
|
||||
//queryFilters.setShowReadItems(showReadItems);
|
||||
}
|
||||
|
||||
public boolean showReadItems() {
|
||||
return queryFilters.getShowReadItems();
|
||||
}
|
||||
|
||||
public void setFilterType(FilterType filterType) {
|
||||
queryFilters.setFilterType(filterType);
|
||||
public void setFilterType(MainFilter filterType) {
|
||||
//queryFilters.setMainFilter(filterType);
|
||||
}
|
||||
|
||||
public FilterType getFilterType() {
|
||||
return queryFilters.getFilterType();
|
||||
public MainFilter getFilterType() {
|
||||
return queryFilters.getMainFilter();
|
||||
}
|
||||
|
||||
public void setSortType(ListSortType sortType) {
|
||||
queryFilters.setSortType(sortType);
|
||||
//queryFilters.setSortType(sortType);
|
||||
}
|
||||
|
||||
public ListSortType getSortType() {
|
||||
@ -113,11 +113,11 @@ public class MainViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void setFilterFeedId(int filterFeedId) {
|
||||
queryFilters.setFilterFeedId(filterFeedId);
|
||||
//queryFilters.setFilterFeedId(filterFeedId);
|
||||
}
|
||||
|
||||
public void setFilerFolderId(int folderId) {
|
||||
queryFilters.setFilterFolderId(folderId);
|
||||
//queryFilters.setFilterFolderId(folderId);
|
||||
}
|
||||
|
||||
public MediatorLiveData<PagedList<ItemWithFeed>> getItemsWithFeed() {
|
||||
@ -128,7 +128,7 @@ public class MainViewModel extends ViewModel {
|
||||
itemsWithFeed.removeSource(lastFetch);
|
||||
|
||||
// get current viewed feed
|
||||
if (feeds == null && queryFilters.getFilterType() == FilterType.FEED_FILTER) {
|
||||
if (feeds == null && queryFilters.getMainFilter() == MainFilter.ALL) {
|
||||
return Single.<Feed>create(emitter -> emitter.onSuccess(database.feedDao()
|
||||
.getFeedById(queryFilters.getFilterFeedId())))
|
||||
.flatMapCompletable(feed -> repository.sync(Collections.singletonList(feed), update));
|
||||
@ -181,7 +181,7 @@ public class MainViewModel extends ViewModel {
|
||||
public void setCurrentAccount(Account currentAccount) {
|
||||
this.currentAccount = currentAccount;
|
||||
setRepository();
|
||||
queryFilters.setAccountId(currentAccount.getId());
|
||||
//queryFilters.setAccountId(currentAccount.getId());
|
||||
buildPagedList();
|
||||
|
||||
// set the new account as the current one
|
||||
@ -212,7 +212,7 @@ public class MainViewModel extends ViewModel {
|
||||
currentAccountExists = true;
|
||||
|
||||
setRepository();
|
||||
queryFilters.setAccountId(currentAccount.getId());
|
||||
//queryFilters.setAccountId(currentAccount.getId());
|
||||
buildPagedList();
|
||||
break;
|
||||
}
|
||||
@ -252,7 +252,7 @@ public class MainViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public Completable setAllItemsReadState(boolean read) {
|
||||
if (queryFilters.getFilterType() == FilterType.FEED_FILTER)
|
||||
if (queryFilters.getMainFilter() == MainFilter.ALL)
|
||||
return repository.setAllFeedItemsReadState(queryFilters.getFilterFeedId(), read);
|
||||
else
|
||||
return repository.setAllItemsReadState(read);
|
||||
|
@ -68,50 +68,22 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2023.10.01')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.compose.activity)
|
||||
|
||||
implementation 'androidx.palette:palette-ktx:1.0.0'
|
||||
implementation 'androidx.activity:activity-compose:1.7.2'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
|
||||
implementation "com.google.accompanist:accompanist-swiperefresh:0.30.1"
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.bundles.lifecycle)
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
def voyager = "1.0.0-rc03"
|
||||
implementation "cafe.adriel.voyager:voyager-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-androidx:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-koin:$voyager"
|
||||
implementation "cafe.adriel.voyager:voyager-transitions:$voyager"
|
||||
implementation(libs.bundles.coroutines)
|
||||
androidTestImplementation(libs.coroutines.test)
|
||||
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:1.4.3"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:1.4.3"
|
||||
implementation(libs.bundles.room)
|
||||
|
||||
def lifecycle_version = "2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
|
||||
|
||||
/*def koin_version = "3.3.3"
|
||||
implementation "io.insert-koin:koin-core:$koin_version"
|
||||
implementation "io.insert-koin:koin-android:$koin_version"
|
||||
implementation "io.insert-koin:koin-androidx-compose:3.4.2"*/
|
||||
|
||||
androidTestImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version"
|
||||
androidTestImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
implementation(libs.bundles.koin)
|
||||
androidTestImplementation(libs.bundles.kointest)
|
||||
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
|
||||
implementation "io.coil-kt:coil:2.4.0"
|
||||
implementation "io.coil-kt:coil-compose:2.4.0"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.1"
|
||||
}
|
@ -10,7 +10,8 @@ import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import kotlinx.coroutines.launch
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.junit.Before
|
||||
@ -48,31 +49,36 @@ class GetFoldersWithFeedsTest {
|
||||
.insert(Feed(name = "Feed ${time + 2}", folderId = 1, accountId = account.id))
|
||||
}
|
||||
|
||||
// inserting 3 items linked to first feed (Feed 0)
|
||||
// inserting 3 unread items linked to first feed (Feed 0)
|
||||
repeat(3) { time ->
|
||||
database.newItemDao()
|
||||
.insert(Item(title = "Item $time", feedId = 1, pubDate = LocalDateTime.now()))
|
||||
}
|
||||
|
||||
// insert 3 read items items linked to second feed (feed 1)
|
||||
repeat(3) { time ->
|
||||
database.newItemDao()
|
||||
.insert(
|
||||
Item(
|
||||
title = "Item ${time + 3}",
|
||||
feedId = 3,
|
||||
isRead = true,
|
||||
pubDate = LocalDateTime.now()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFoldersWithFeedsTest() = runTest {
|
||||
getFoldersWithFeeds = GetFoldersWithFeeds(database)
|
||||
val job = launch {
|
||||
getFoldersWithFeeds.get(account.id)
|
||||
.collect { foldersAndFeeds ->
|
||||
val foldersAndFeeds = getFoldersWithFeeds.get(account.id, MainFilter.ALL).first()
|
||||
|
||||
|
||||
assertTrue { foldersAndFeeds.size == 4 }
|
||||
assertTrue { foldersAndFeeds.entries.first().value.size == 2 }
|
||||
assertTrue { foldersAndFeeds.entries.last().key == null }
|
||||
assertTrue { foldersAndFeeds[null]!!.size == 2 }
|
||||
assertTrue { foldersAndFeeds[null]!!.first().unreadCount == 3 }
|
||||
}
|
||||
}
|
||||
|
||||
// for an unknown reason, the coroutine must be canceled to stop the test, and I don't really know why
|
||||
job.cancel()
|
||||
assertTrue { foldersAndFeeds.size == 4 }
|
||||
assertTrue { foldersAndFeeds.entries.first().value.size == 2 }
|
||||
assertTrue { foldersAndFeeds.entries.last().key == null }
|
||||
assertTrue { foldersAndFeeds[null]!!.size == 2 }
|
||||
assertTrue { foldersAndFeeds[null]!!.first().unreadCount == 3 }
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ class LocalRSSRepositoryTest : KoinTest {
|
||||
.setBody(Buffer().readFrom(stream))
|
||||
)
|
||||
|
||||
val result = repository.synchronize(null) {
|
||||
val result = repository.synchronize(listOf()) {
|
||||
assertEquals(it.name, feeds.first().name)
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,24 @@
|
||||
package com.readrops.app.compose
|
||||
|
||||
import com.readrops.app.compose.account.AccountViewModel
|
||||
import com.readrops.app.compose.account.AccountScreenModel
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||
import com.readrops.app.compose.feeds.FeedViewModel
|
||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||
import com.readrops.app.compose.timelime.TimelineViewModel
|
||||
import com.readrops.app.compose.timelime.TimelineScreenModel
|
||||
import com.readrops.db.entities.account.Account
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val composeAppModule = module {
|
||||
|
||||
viewModel { TimelineViewModel(get(), get()) }
|
||||
factory { TimelineScreenModel(get(), get()) }
|
||||
|
||||
viewModel { FeedViewModel(get(), get(), get()) }
|
||||
factory { FeedScreenModel(get(), get(), get()) }
|
||||
|
||||
viewModel { AccountSelectionViewModel(get()) }
|
||||
factory { AccountSelectionViewModel(get()) }
|
||||
|
||||
viewModel { AccountViewModel(get()) }
|
||||
factory { AccountScreenModel(get()) }
|
||||
|
||||
single { GetFoldersWithFeeds(get()) }
|
||||
|
||||
|
@ -3,10 +3,9 @@ package com.readrops.app.compose
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.material3.*
|
||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionScreen
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
@ -15,7 +14,6 @@ import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@ -25,7 +23,11 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
ReadropsTheme {
|
||||
Navigator(
|
||||
screen = if (accountExists) HomeScreen() else AccountSelectionScreen()
|
||||
screen = if (accountExists) HomeScreen() else AccountSelectionScreen(),
|
||||
disposeBehavior = NavigatorDisposeBehavior(
|
||||
// prevent screenModels being recreated when opening a screen from a tab
|
||||
disposeNestedNavigators = false
|
||||
)
|
||||
) {
|
||||
CurrentScreen()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.readrops.app.compose.account
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.readrops.app.compose.base.TabViewModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AccountViewModel(
|
||||
class AccountScreenModel(
|
||||
private val database: Database
|
||||
) : TabViewModel(database) {
|
||||
) : TabScreenModel(database) {
|
||||
|
||||
private val _closeHome = MutableStateFlow(false)
|
||||
val closeHome = _closeHome.asStateFlow()
|
||||
@ -22,7 +22,7 @@ class AccountViewModel(
|
||||
val accountState = _accountState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
accountEvent.collect { account ->
|
||||
_accountState.update {
|
||||
it.copy(
|
||||
@ -38,7 +38,7 @@ class AccountViewModel(
|
||||
fun closeDialog() = _accountState.update { it.copy(dialog = null) }
|
||||
|
||||
fun deleteAccount() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
database.newAccountDao()
|
||||
.delete(currentAccount!!)
|
||||
|
@ -28,6 +28,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
@ -41,7 +42,6 @@ import com.readrops.app.compose.util.components.TwoChoicesDialog
|
||||
import com.readrops.app.compose.util.theme.LargeSpacer
|
||||
import com.readrops.app.compose.util.theme.MediumSpacer
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
object AccountTab : Tab {
|
||||
|
||||
@ -56,7 +56,7 @@ object AccountTab : Tab {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val viewModel = getViewModel<AccountViewModel>()
|
||||
val viewModel = getScreenModel<AccountScreenModel>()
|
||||
|
||||
val closeHome by viewModel.closeHome.collectAsStateWithLifecycle()
|
||||
val state by viewModel.accountState.collectAsStateWithLifecycle()
|
||||
|
@ -8,10 +8,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
import com.readrops.app.compose.util.components.AndroidScreen
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
|
||||
|
@ -16,12 +16,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
import com.readrops.app.compose.util.components.AndroidScreen
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.readrops.app.compose.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.account.Account
|
||||
@ -15,9 +15,9 @@ import org.koin.core.parameter.parametersOf
|
||||
/**
|
||||
* Custom ViewModel for Tab screens handling account change
|
||||
*/
|
||||
abstract class TabViewModel(
|
||||
abstract class TabScreenModel(
|
||||
private val database: Database,
|
||||
) : ViewModel(), KoinComponent {
|
||||
) : ScreenModel, KoinComponent {
|
||||
|
||||
/**
|
||||
* Repository intended to be rebuilt when the current account changes
|
||||
@ -29,7 +29,7 @@ abstract class TabViewModel(
|
||||
protected val accountEvent = MutableSharedFlow<Account>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
screenModelScope.launch {
|
||||
database.newAccountDao()
|
||||
.selectCurrentAccount()
|
||||
.distinctUntilChanged()
|
@ -12,8 +12,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import coil.compose.AsyncImage
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.util.theme.ShortSpacer
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import com.readrops.app.compose.util.toDp
|
||||
@ -43,6 +45,7 @@ fun FeedItem(
|
||||
) {
|
||||
AsyncImage(
|
||||
model = feed.iconUrl,
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
contentDescription = feed.name!!,
|
||||
modifier = Modifier.size(MaterialTheme.typography.bodyLarge.toDp())
|
||||
)
|
||||
|
@ -1,16 +1,17 @@
|
||||
package com.readrops.app.compose.feeds
|
||||
|
||||
import android.util.Patterns
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||
import com.readrops.api.utils.HtmlParser
|
||||
import com.readrops.app.compose.base.TabViewModel
|
||||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||
import com.readrops.app.compose.util.components.TextFieldError
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -24,11 +25,11 @@ import org.koin.core.component.get
|
||||
import java.net.UnknownHostException
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class FeedViewModel(
|
||||
class FeedScreenModel(
|
||||
database: Database,
|
||||
private val getFoldersWithFeeds: GetFoldersWithFeeds,
|
||||
private val localRSSDataSource: LocalRSSDataSource,
|
||||
) : TabViewModel(database), KoinComponent {
|
||||
) : TabScreenModel(database), KoinComponent {
|
||||
|
||||
private val _feedState = MutableStateFlow(FeedState())
|
||||
val feedsState = _feedState.asStateFlow()
|
||||
@ -43,10 +44,10 @@ class FeedViewModel(
|
||||
val folderState = _folderState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
screenModelScope.launch(context = Dispatchers.IO) {
|
||||
accountEvent
|
||||
.flatMapConcat { account ->
|
||||
getFoldersWithFeeds.get(account.id)
|
||||
getFoldersWithFeeds.get(account.id, MainFilter.ALL)
|
||||
}
|
||||
.catch { throwable ->
|
||||
_feedState.update {
|
||||
@ -60,7 +61,7 @@ class FeedViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
screenModelScope.launch(context = Dispatchers.IO) {
|
||||
database.newAccountDao()
|
||||
.selectAllAccounts()
|
||||
.collect { accounts ->
|
||||
@ -73,7 +74,7 @@ class FeedViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
screenModelScope.launch(context = Dispatchers.IO) {
|
||||
accountEvent
|
||||
.flatMapConcat { account ->
|
||||
database.newFolderDao()
|
||||
@ -93,7 +94,25 @@ class FeedViewModel(
|
||||
fun setFolderExpandState(isExpanded: Boolean) =
|
||||
_feedState.update { it.copy(areFoldersExpanded = isExpanded) }
|
||||
|
||||
fun closeDialog() = _feedState.update { it.copy(dialog = null) }
|
||||
fun closeDialog(dialog: DialogState? = null) {
|
||||
if (dialog is DialogState.AddFeed) {
|
||||
_addFeedDialogState.update {
|
||||
it.copy(
|
||||
url = "",
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} else if (dialog is DialogState.AddFolder || dialog is DialogState.UpdateFolder) {
|
||||
_folderState.update {
|
||||
it.copy(
|
||||
folder = Folder(),
|
||||
nameError = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_feedState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun openDialog(state: DialogState) {
|
||||
if (state is DialogState.UpdateFeed) {
|
||||
@ -119,13 +138,13 @@ class FeedViewModel(
|
||||
}
|
||||
|
||||
fun deleteFeed(feed: Feed) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
repository?.deleteFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFolder(folder: Folder) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
repository?.deleteFolder(folder)
|
||||
}
|
||||
}
|
||||
@ -165,13 +184,13 @@ class FeedViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
else -> viewModelScope.launch(Dispatchers.IO) {
|
||||
else -> screenModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (localRSSDataSource.isUrlRSSResource(url)) {
|
||||
// TODO add support for all account types
|
||||
repository?.insertNewFeeds(listOf(url))
|
||||
|
||||
closeDialog()
|
||||
closeDialog(DialogState.AddFeed)
|
||||
} else {
|
||||
val rssUrls = HtmlParser.getFeedLink(url, get())
|
||||
|
||||
@ -183,7 +202,7 @@ class FeedViewModel(
|
||||
// TODO add support for all account types
|
||||
repository?.insertNewFeeds(rssUrls.map { it.url })
|
||||
|
||||
closeDialog()
|
||||
closeDialog(DialogState.AddFeed)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -196,15 +215,6 @@ class FeedViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAddFeedDialogState() {
|
||||
_addFeedDialogState.update {
|
||||
it.copy(
|
||||
url = "",
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// add feed
|
||||
|
||||
// update feed
|
||||
@ -266,7 +276,7 @@ class FeedViewModel(
|
||||
}
|
||||
|
||||
else -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
with(_updateFeedDialogState.value) {
|
||||
repository?.updateFeed(
|
||||
Feed(
|
||||
@ -295,13 +305,6 @@ class FeedViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun resetFolderState() = _folderState.update {
|
||||
it.copy(
|
||||
folder = Folder(),
|
||||
nameError = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun folderValidate(updateFolder: Boolean = false) {
|
||||
val name = _folderState.value.name.orEmpty()
|
||||
|
||||
@ -311,7 +314,7 @@ class FeedViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
if (updateFolder) {
|
||||
repository?.updateFolder(_folderState.value.folder)
|
||||
} else {
|
||||
@ -320,8 +323,7 @@ class FeedViewModel(
|
||||
})
|
||||
}
|
||||
|
||||
closeDialog()
|
||||
resetFolderState()
|
||||
closeDialog(DialogState.AddFolder)
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import com.readrops.app.compose.R
|
||||
@ -43,7 +44,6 @@ import com.readrops.app.compose.util.components.Placeholder
|
||||
import com.readrops.app.compose.util.components.TwoChoicesDialog
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import com.readrops.db.entities.Feed
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
object FeedTab : Tab {
|
||||
|
||||
@ -59,7 +59,8 @@ object FeedTab : Tab {
|
||||
override fun Content() {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val viewModel = getViewModel<FeedViewModel>()
|
||||
|
||||
val viewModel = getScreenModel<FeedScreenModel>()
|
||||
|
||||
val state by viewModel.feedsState.collectAsStateWithLifecycle()
|
||||
|
||||
@ -68,8 +69,7 @@ object FeedTab : Tab {
|
||||
AddFeedDialog(
|
||||
viewModel = viewModel,
|
||||
onDismiss = {
|
||||
viewModel.closeDialog()
|
||||
viewModel.resetAddFeedDialogState()
|
||||
viewModel.closeDialog(DialogState.AddFeed)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -122,8 +122,7 @@ object FeedTab : Tab {
|
||||
FolderDialog(
|
||||
viewModel = viewModel,
|
||||
onDismiss = {
|
||||
viewModel.closeDialog()
|
||||
viewModel.resetFolderState()
|
||||
viewModel.closeDialog(DialogState.AddFolder)
|
||||
},
|
||||
onValidate = {
|
||||
viewModel.folderValidate()
|
||||
@ -151,8 +150,7 @@ object FeedTab : Tab {
|
||||
updateFolder = true,
|
||||
viewModel = viewModel,
|
||||
onDismiss = {
|
||||
viewModel.closeDialog()
|
||||
viewModel.resetFolderState()
|
||||
viewModel.closeDialog(DialogState.UpdateFolder(dialog.folder))
|
||||
},
|
||||
onValidate = {
|
||||
viewModel.folderValidate(updateFolder = true)
|
||||
|
@ -21,7 +21,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.feeds.FeedViewModel
|
||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
||||
import com.readrops.app.compose.util.components.BaseDialog
|
||||
import com.readrops.app.compose.util.theme.LargeSpacer
|
||||
import com.readrops.app.compose.util.theme.ShortSpacer
|
||||
@ -29,7 +29,7 @@ import com.readrops.app.compose.util.theme.ShortSpacer
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddFeedDialog(
|
||||
viewModel: FeedViewModel,
|
||||
viewModel: FeedScreenModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.addFeedDialogState.collectAsStateWithLifecycle()
|
||||
|
@ -13,14 +13,14 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.feeds.FeedViewModel
|
||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
||||
import com.readrops.app.compose.util.components.BaseDialog
|
||||
import com.readrops.app.compose.util.theme.LargeSpacer
|
||||
|
||||
@Composable
|
||||
fun FolderDialog(
|
||||
updateFolder: Boolean = false,
|
||||
viewModel: FeedViewModel,
|
||||
viewModel: FeedScreenModel,
|
||||
onDismiss: () -> Unit,
|
||||
onValidate: () -> Unit
|
||||
) {
|
||||
|
@ -15,7 +15,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.feeds.FeedViewModel
|
||||
import com.readrops.app.compose.feeds.FeedScreenModel
|
||||
import com.readrops.app.compose.util.components.BaseDialog
|
||||
import com.readrops.app.compose.util.theme.LargeSpacer
|
||||
import com.readrops.app.compose.util.theme.MediumSpacer
|
||||
@ -24,7 +24,7 @@ import com.readrops.app.compose.util.theme.MediumSpacer
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UpdateFeedDialog(
|
||||
viewModel: FeedViewModel,
|
||||
viewModel: FeedScreenModel,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val state by viewModel.updateFeedDialogState.collectAsStateWithLifecycle()
|
||||
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.CurrentTab
|
||||
@ -26,6 +25,7 @@ import com.readrops.app.compose.account.AccountTab
|
||||
import com.readrops.app.compose.feeds.FeedTab
|
||||
import com.readrops.app.compose.more.MoreTab
|
||||
import com.readrops.app.compose.timelime.TimelineTab
|
||||
import com.readrops.app.compose.util.components.AndroidScreen
|
||||
|
||||
class HomeScreen : AndroidScreen() {
|
||||
|
||||
|
@ -2,7 +2,7 @@ package com.readrops.app.compose.item
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import com.readrops.app.compose.util.components.AndroidScreen
|
||||
|
||||
class ItemScreen : AndroidScreen() {
|
||||
|
||||
|
@ -7,9 +7,7 @@ import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
|
||||
data class ErrorResult(
|
||||
val values: Map<Feed, Exception>
|
||||
)
|
||||
typealias ErrorResult = Map<Feed, Exception>
|
||||
|
||||
abstract class ARepository(
|
||||
val database: Database,
|
||||
@ -29,7 +27,7 @@ abstract class ARepository(
|
||||
* and errors per feed if occurred to be transmitted to the user
|
||||
*/
|
||||
abstract suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>?,
|
||||
selectedFeeds: List<Feed>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): Pair<SyncResult, ErrorResult>
|
||||
|
||||
@ -73,6 +71,10 @@ abstract class BaseRepository(
|
||||
database.newItemDao().setAllStarredItemsRead(accountId)
|
||||
}
|
||||
|
||||
open suspend fun setAllNewItemsRead(accountId: Int) {
|
||||
database.newItemDao().setAllNewItemsRead(accountId)
|
||||
}
|
||||
|
||||
open suspend fun setAllItemsReadByFeed(feedId: Int, accountId: Int) {
|
||||
database.newItemDao().setAllItemsReadByFeed(feedId, accountId)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.readrops.app.compose.repositories
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@ -10,12 +12,17 @@ class GetFoldersWithFeeds(
|
||||
private val database: Database,
|
||||
) {
|
||||
|
||||
fun get(accountId: Int): Flow<Map<Folder?, List<Feed>>> {
|
||||
fun get(accountId: Int, mainFilter: MainFilter): Flow<Map<Folder?, List<Feed>>> {
|
||||
val foldersAndFeedsQuery =
|
||||
FoldersAndFeedsQueriesBuilder.buildFoldersAndFeedsQuery(accountId, mainFilter)
|
||||
val feedsWithoutFolderQuery =
|
||||
FoldersAndFeedsQueriesBuilder.buildFeedsWithoutFolderQuery(accountId, mainFilter)
|
||||
|
||||
return combine(
|
||||
flow = database.newFolderDao()
|
||||
.selectFoldersAndFeeds(accountId),
|
||||
.selectFoldersAndFeeds(foldersAndFeedsQuery),
|
||||
flow2 = database.newFeedDao()
|
||||
.selectFeedsWithoutFolder(accountId)
|
||||
.selectFeedsWithoutFolder(feedsWithoutFolderQuery)
|
||||
) { folders, feedsWithoutFolder ->
|
||||
val foldersWithFeeds = folders.groupBy(
|
||||
keySelector = {
|
||||
@ -47,7 +54,16 @@ class GetFoldersWithFeeds(
|
||||
foldersWithFeeds + mapOf(
|
||||
Pair(
|
||||
null,
|
||||
feedsWithoutFolder.map { it.feed.apply { unreadCount = it.unreadCount } })
|
||||
feedsWithoutFolder.map { feedWithoutFolder ->
|
||||
Feed(
|
||||
id = feedWithoutFolder.feedId,
|
||||
name = feedWithoutFolder.feedName,
|
||||
iconUrl = feedWithoutFolder.feedIcon,
|
||||
url = feedWithoutFolder.feedUrl,
|
||||
siteUrl = feedWithoutFolder.feedSiteUrl,
|
||||
unreadCount = feedWithoutFolder.unreadCount
|
||||
)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
foldersWithFeeds
|
||||
|
@ -27,15 +27,15 @@ class LocalRSSRepository(
|
||||
}
|
||||
|
||||
override suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>?,
|
||||
selectedFeeds: List<Feed>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): Pair<SyncResult, ErrorResult> {
|
||||
val errors = mutableMapOf<Feed, Exception>()
|
||||
val syncResult = SyncResult()
|
||||
|
||||
val feeds = if (selectedFeeds.isNullOrEmpty()) {
|
||||
val feeds = selectedFeeds.ifEmpty {
|
||||
database.newFeedDao().selectFeeds(account.id)
|
||||
} else selectedFeeds
|
||||
}
|
||||
|
||||
for (feed in feeds) {
|
||||
onUpdate(feed)
|
||||
@ -61,7 +61,7 @@ class LocalRSSRepository(
|
||||
|
||||
}
|
||||
|
||||
return Pair(syncResult, ErrorResult(errors))
|
||||
return Pair(syncResult, errors)
|
||||
}
|
||||
|
||||
override suspend fun synchronize(): SyncResult =
|
||||
|
@ -0,0 +1,67 @@
|
||||
package com.readrops.app.compose.timelime
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.readrops.api.utils.exceptions.HttpException
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.repositories.ErrorResult
|
||||
import com.readrops.app.compose.util.components.BaseDialog
|
||||
import com.readrops.app.compose.util.theme.MediumSpacer
|
||||
import com.readrops.app.compose.util.theme.ShortSpacer
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
@Composable
|
||||
fun ErrorListDialog(
|
||||
errorResult: ErrorResult,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val scrollableState = rememberScrollState()
|
||||
|
||||
BaseDialog(
|
||||
title = stringResource(R.string.synchronization_errors),
|
||||
icon = painterResource(id = R.drawable.ic_error),
|
||||
onDismiss = onDismiss,
|
||||
modifier = Modifier.heightIn(max = 500.dp)
|
||||
) {
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.error_occurred_feed,
|
||||
count = errorResult.size
|
||||
)
|
||||
)
|
||||
|
||||
MediumSpacer()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(scrollableState)
|
||||
) {
|
||||
for (error in errorResult.entries) {
|
||||
Text(text = "${error.key.name}: ${errorText(error.value)}")
|
||||
|
||||
ShortSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check compatibility with other accounts errors
|
||||
@Composable
|
||||
fun errorText(exception: Exception) = when (exception) {
|
||||
is HttpException -> stringResource(id = R.string.unreachable_feed_http_error, exception.code.toString())
|
||||
is UnknownHostException -> stringResource(R.string.unreachable_feed)
|
||||
is IOException -> stringResource(R.string.network_failure, exception.message.orEmpty())
|
||||
is ParseException, is UnknownFormatException -> stringResource(R.string.processing_feed_error)
|
||||
else -> "${exception.javaClass.simpleName}: ${exception.message}"
|
||||
}
|
@ -24,7 +24,8 @@ import com.readrops.db.queries.QueryFilters
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FilterBottomSheet(
|
||||
viewModel: TimelineViewModel,
|
||||
onSetShowReadItemsState: () -> Unit,
|
||||
onSetSortTypeState: () -> Unit,
|
||||
filters: QueryFilters,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@ -44,11 +45,11 @@ fun FilterBottomSheet(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { viewModel.setShowReadItemsState(!filters.showReadItems) }
|
||||
.clickable(onClick = onSetShowReadItemsState)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = filters.showReadItems,
|
||||
onCheckedChange = { viewModel.setShowReadItemsState(!filters.showReadItems) }
|
||||
onCheckedChange = { onSetShowReadItemsState() }
|
||||
)
|
||||
|
||||
ShortSpacer()
|
||||
@ -60,24 +61,15 @@ fun FilterBottomSheet(
|
||||
|
||||
ShortSpacer()
|
||||
|
||||
fun setSortTypeState() {
|
||||
viewModel.setSortTypeState(
|
||||
if (filters.sortType == ListSortType.NEWEST_TO_OLDEST)
|
||||
ListSortType.OLDEST_TO_NEWEST
|
||||
else
|
||||
ListSortType.NEWEST_TO_OLDEST
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { setSortTypeState() }
|
||||
.clickable(onClick = onSetSortTypeState)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = filters.sortType == ListSortType.OLDEST_TO_NEWEST,
|
||||
onCheckedChange = { setSortTypeState() }
|
||||
onCheckedChange = { onSetSortTypeState() }
|
||||
)
|
||||
|
||||
ShortSpacer()
|
||||
|
@ -43,7 +43,6 @@ fun TimelineItem(
|
||||
itemWithFeed: ItemWithFeed,
|
||||
onClick: () -> Unit,
|
||||
onFavorite: () -> Unit,
|
||||
onReadLater: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
compactLayout: Boolean = false,
|
||||
@ -75,7 +74,8 @@ fun TimelineItem(
|
||||
) {
|
||||
AsyncImage(
|
||||
model = itemWithFeed.feedIconUrl,
|
||||
contentDescription = null,
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
contentDescription = itemWithFeed.feedName,
|
||||
placeholder = painterResource(R.drawable.ic_rss_feed_grey),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
@ -188,12 +188,6 @@ fun TimelineItem(
|
||||
modifier = Modifier.clickable { onFavorite() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_read_later),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable { onReadLater() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Share,
|
||||
contentDescription = null,
|
||||
|
@ -3,19 +3,21 @@ package com.readrops.app.compose.timelime
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.readrops.app.compose.base.TabViewModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.app.compose.repositories.ErrorResult
|
||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.filters.FilterType
|
||||
import com.readrops.db.filters.ListSortType
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import com.readrops.db.filters.SubFilter
|
||||
import com.readrops.db.pojo.ItemWithFeed
|
||||
import com.readrops.db.queries.ItemsQueryBuilder
|
||||
import com.readrops.db.queries.QueryFilters
|
||||
@ -30,11 +32,11 @@ import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TimelineViewModel(
|
||||
class TimelineScreenModel(
|
||||
private val database: Database,
|
||||
private val getFoldersWithFeeds: GetFoldersWithFeeds,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : TabViewModel(database) {
|
||||
) : TabScreenModel(database) {
|
||||
|
||||
private val _timelineState = MutableStateFlow(TimelineState())
|
||||
val timelineState = _timelineState.asStateFlow()
|
||||
@ -42,13 +44,12 @@ class TimelineViewModel(
|
||||
private val filters = MutableStateFlow(_timelineState.value.filters)
|
||||
|
||||
init {
|
||||
viewModelScope.launch(dispatcher) {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
combine(
|
||||
accountEvent,
|
||||
filters
|
||||
) { account, filters ->
|
||||
filters.accountId = account.id
|
||||
Pair(account, filters)
|
||||
Pair(account, filters.copy(accountId = account.id))
|
||||
}.collectLatest { (account, filters) ->
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(filters)
|
||||
|
||||
@ -63,11 +64,11 @@ class TimelineViewModel(
|
||||
database.newItemDao().selectAll(query)
|
||||
},
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
.cachedIn(screenModelScope)
|
||||
)
|
||||
}
|
||||
|
||||
getFoldersWithFeeds.get(account.id)
|
||||
getFoldersWithFeeds.get(account.id, filters.mainFilter)
|
||||
.collect { foldersAndFeeds ->
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
@ -80,16 +81,49 @@ class TimelineViewModel(
|
||||
}
|
||||
|
||||
fun refreshTimeline() {
|
||||
_timelineState.update { it.copy(isRefreshing = true) }
|
||||
viewModelScope.launch(dispatcher) {
|
||||
repository?.synchronize(null) {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
val selectedFeeds = if (currentAccount!!.isLocal) {
|
||||
when (filters.value.subFilter) {
|
||||
SubFilter.FEED -> listOf(
|
||||
database.newFeedDao().selectFeed(filters.value.filterFeedId)
|
||||
)
|
||||
|
||||
SubFilter.FOLDER -> database.newFeedDao()
|
||||
.selectFeedsByFolder(filters.value.filterFolderId)
|
||||
|
||||
else -> listOf()
|
||||
}
|
||||
} else listOf()
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
feedCount = 0,
|
||||
feedMax = if (selectedFeeds.isNotEmpty())
|
||||
selectedFeeds.size
|
||||
else
|
||||
database.newFeedDao().selectFeedCount(currentAccount!!.id)
|
||||
)
|
||||
}
|
||||
|
||||
_timelineState.update { it.copy(isRefreshing = true) }
|
||||
|
||||
val results = repository?.synchronize(
|
||||
selectedFeeds = selectedFeeds,
|
||||
onUpdate = { feed ->
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
currentFeed = feed.name!!,
|
||||
feedCount = it.feedCount + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
isRefreshing = false,
|
||||
endSynchronizing = true
|
||||
endSynchronizing = true,
|
||||
synchronizationErrors = if (results!!.second.isNotEmpty()) results.second else null
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -103,12 +137,15 @@ class TimelineViewModel(
|
||||
_timelineState.update { it.copy(isDrawerOpen = false) }
|
||||
}
|
||||
|
||||
fun updateDrawerDefaultItem(selection: FilterType) {
|
||||
fun updateDrawerDefaultItem(selection: MainFilter) {
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
filters = updateFilters {
|
||||
it.filters.copy(
|
||||
filterType = selection
|
||||
mainFilter = selection,
|
||||
subFilter = SubFilter.ALL,
|
||||
filterFeedId = 0,
|
||||
filterFolderId = 0
|
||||
)
|
||||
},
|
||||
isDrawerOpen = false
|
||||
@ -121,7 +158,7 @@ class TimelineViewModel(
|
||||
it.copy(
|
||||
filters = updateFilters {
|
||||
it.filters.copy(
|
||||
filterType = FilterType.FOLDER_FILER,
|
||||
subFilter = SubFilter.FOLDER,
|
||||
filterFolderId = folder.id,
|
||||
filterFeedId = 0
|
||||
)
|
||||
@ -137,7 +174,7 @@ class TimelineViewModel(
|
||||
it.copy(
|
||||
filters = updateFilters {
|
||||
it.filters.copy(
|
||||
filterType = FilterType.FEED_FILTER,
|
||||
subFilter = SubFilter.FEED,
|
||||
filterFeedId = feed.id,
|
||||
filterFolderId = 0
|
||||
)
|
||||
@ -161,13 +198,13 @@ class TimelineViewModel(
|
||||
}
|
||||
|
||||
private fun updateItemReadState(item: Item) {
|
||||
viewModelScope.launch(dispatcher) {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
repository?.setItemReadState(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStarState(item: Item) {
|
||||
viewModelScope.launch(dispatcher) {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
with(item) {
|
||||
isStarred = isStarred.not()
|
||||
repository?.setItemStarState(this)
|
||||
@ -186,30 +223,39 @@ class TimelineViewModel(
|
||||
}
|
||||
|
||||
fun setAllItemsRead() {
|
||||
viewModelScope.launch(dispatcher) {
|
||||
when (_timelineState.value.filters.filterType) {
|
||||
FilterType.FEED_FILTER ->
|
||||
screenModelScope.launch(dispatcher) {
|
||||
val accountId = currentAccount!!.id
|
||||
|
||||
when (_timelineState.value.filters.subFilter) {
|
||||
SubFilter.FEED ->
|
||||
repository?.setAllItemsReadByFeed(
|
||||
_timelineState.value.filters.filterFeedId,
|
||||
currentAccount!!.id
|
||||
feedId = _timelineState.value.filters.filterFeedId,
|
||||
accountId = accountId
|
||||
)
|
||||
|
||||
FilterType.FOLDER_FILER -> repository?.setAllItemsReadByFolder(
|
||||
_timelineState.value.filters.filterFolderId,
|
||||
currentAccount!!.id
|
||||
SubFilter.FOLDER -> repository?.setAllItemsReadByFolder(
|
||||
folderId = _timelineState.value.filters.filterFolderId,
|
||||
accountId = accountId
|
||||
)
|
||||
|
||||
FilterType.READ_IT_LATER_FILTER -> TODO()
|
||||
FilterType.STARS_FILTER -> repository?.setAllStarredItemsRead(currentAccount!!.id)
|
||||
FilterType.NO_FILTER -> repository?.setAllItemsRead(currentAccount!!.id)
|
||||
FilterType.NEW -> TODO()
|
||||
else -> when (_timelineState.value.filters.mainFilter) {
|
||||
MainFilter.STARS -> repository?.setAllStarredItemsRead(accountId)
|
||||
MainFilter.ALL -> repository?.setAllItemsRead(accountId)
|
||||
MainFilter.NEW -> repository?.setAllNewItemsRead(accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openDialog(dialog: DialogState) = _timelineState.update { it.copy(dialog = dialog) }
|
||||
|
||||
fun closeDialog() = _timelineState.update { it.copy(dialog = null) }
|
||||
fun closeDialog(dialog: DialogState? = null) {
|
||||
if (dialog is DialogState.ErrorList) {
|
||||
_timelineState.update { it.copy(synchronizationErrors = null) }
|
||||
}
|
||||
|
||||
_timelineState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun setShowReadItemsState(showReadItems: Boolean) {
|
||||
_timelineState.update {
|
||||
@ -234,22 +280,32 @@ class TimelineViewModel(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TimelineState(
|
||||
val isRefreshing: Boolean = false,
|
||||
val isDrawerOpen: Boolean = false,
|
||||
val currentFeed: String = "",
|
||||
val feedCount: Int = 0,
|
||||
val feedMax: Int = 0,
|
||||
val endSynchronizing: Boolean = false,
|
||||
val synchronizationErrors: ErrorResult? = null,
|
||||
val filters: QueryFilters = QueryFilters(),
|
||||
val filterFeedName: String = "",
|
||||
val filterFolderName: String = "",
|
||||
val foldersAndFeeds: Map<Folder?, List<Feed>> = emptyMap(),
|
||||
val itemState: Flow<PagingData<ItemWithFeed>> = emptyFlow(),
|
||||
val dialog: DialogState? = null
|
||||
)
|
||||
) {
|
||||
|
||||
val showSubtitle = filters.subFilter != SubFilter.ALL
|
||||
}
|
||||
|
||||
sealed interface DialogState {
|
||||
object ConfirmDialog : DialogState
|
||||
object FilterSheet : DialogState
|
||||
class ErrorList(val errorResult: ErrorResult) : DialogState
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
package com.readrops.app.compose.timelime
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@ -18,34 +19,49 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.item.ItemScreen
|
||||
import com.readrops.app.compose.timelime.drawer.TimelineDrawer
|
||||
import com.readrops.app.compose.util.components.CenteredColumn
|
||||
import com.readrops.app.compose.util.components.CenteredProgressIndicator
|
||||
import com.readrops.app.compose.util.components.Placeholder
|
||||
import com.readrops.app.compose.util.components.RefreshScreen
|
||||
import com.readrops.app.compose.util.components.TwoChoicesDialog
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import com.readrops.db.filters.FilterType
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import com.readrops.db.filters.ListSortType
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import com.readrops.db.filters.SubFilter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
object TimelineTab : Tab {
|
||||
@ -60,17 +76,34 @@ object TimelineTab : Tab {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val viewModel = getViewModel<TimelineViewModel>()
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val viewModel = getScreenModel<TimelineScreenModel>()
|
||||
val state by viewModel.timelineState.collectAsStateWithLifecycle()
|
||||
val items = state.itemState.collectAsLazyPagingItems()
|
||||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val scrollState = rememberLazyListState()
|
||||
LaunchedEffect(state.isRefreshing) {
|
||||
if (state.isRefreshing) {
|
||||
pullToRefreshState.startRefresh()
|
||||
} else {
|
||||
pullToRefreshState.endRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Material3 pull to refresh doesn't have a onRefresh callback,
|
||||
// so we need to listen to the internal state change to trigger the refresh
|
||||
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
||||
if (pullToRefreshState.isRefreshing && !state.isRefreshing) {
|
||||
viewModel.refreshTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
// Use the depreciated refresh swipe as the material 3 one isn't available yet
|
||||
val swipeState = rememberSwipeRefreshState(state.isRefreshing)
|
||||
val drawerState = rememberDrawerState(
|
||||
initialValue = DrawerValue.Closed,
|
||||
confirmStateChange = {
|
||||
@ -97,8 +130,27 @@ object TimelineTab : Tab {
|
||||
}
|
||||
}
|
||||
|
||||
when (state.dialog) {
|
||||
DialogState.ConfirmDialog -> {
|
||||
LaunchedEffect(state.synchronizationErrors) {
|
||||
if (state.synchronizationErrors != null) {
|
||||
coroutineScope.launch {
|
||||
val action = snackbarHostState.showSnackbar(
|
||||
message = context.resources.getQuantityString(R.plurals.error_occurred, state.synchronizationErrors!!.size),
|
||||
actionLabel = context.getString(R.string.details),
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
|
||||
if (action == SnackbarResult.ActionPerformed) {
|
||||
viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!))
|
||||
} else {
|
||||
// remove errors from state
|
||||
viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is DialogState.ConfirmDialog -> {
|
||||
TwoChoicesDialog(
|
||||
title = "Mark all items as read",
|
||||
text = "Do you really want to mark all items as read?",
|
||||
@ -113,13 +165,28 @@ object TimelineTab : Tab {
|
||||
)
|
||||
}
|
||||
|
||||
DialogState.FilterSheet -> {
|
||||
is DialogState.FilterSheet -> {
|
||||
FilterBottomSheet(
|
||||
viewModel = viewModel,
|
||||
filters = state.filters,
|
||||
onDismiss = {
|
||||
viewModel.closeDialog()
|
||||
}
|
||||
onSetShowReadItemsState = {
|
||||
viewModel.setShowReadItemsState(!state.filters.showReadItems)
|
||||
},
|
||||
onSetSortTypeState = {
|
||||
viewModel.setSortTypeState(
|
||||
if (state.filters.sortType == ListSortType.NEWEST_TO_OLDEST)
|
||||
ListSortType.OLDEST_TO_NEWEST
|
||||
else
|
||||
ListSortType.NEWEST_TO_OLDEST
|
||||
)
|
||||
},
|
||||
onDismiss = { viewModel.closeDialog() }
|
||||
)
|
||||
}
|
||||
|
||||
is DialogState.ErrorList -> {
|
||||
ErrorListDialog(
|
||||
errorResult = dialog.errorResult,
|
||||
onDismiss = { viewModel.closeDialog(state.dialog) }
|
||||
)
|
||||
}
|
||||
|
||||
@ -147,16 +214,30 @@ object TimelineTab : Tab {
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = when (state.filters.filterType) {
|
||||
FilterType.FEED_FILTER -> state.filterFeedName
|
||||
FilterType.FOLDER_FILER -> state.filterFolderName
|
||||
FilterType.READ_IT_LATER_FILTER -> stringResource(R.string.read_later)
|
||||
FilterType.STARS_FILTER -> stringResource(R.string.favorites)
|
||||
FilterType.NO_FILTER -> stringResource(R.string.articles)
|
||||
FilterType.NEW -> stringResource(R.string.new_articles)
|
||||
Column {
|
||||
Text(
|
||||
text = when (state.filters.mainFilter) {
|
||||
MainFilter.STARS -> stringResource(R.string.favorites)
|
||||
MainFilter.ALL -> stringResource(R.string.articles)
|
||||
MainFilter.NEW -> stringResource(R.string.new_articles)
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (state.showSubtitle) {
|
||||
Text(
|
||||
text = when (state.filters.subFilter) {
|
||||
SubFilter.FEED -> state.filterFeedName
|
||||
SubFilter.FOLDER -> state.filterFolderName
|
||||
else -> ""
|
||||
},
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
@ -189,10 +270,11 @@ object TimelineTab : Tab {
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (state.filters.filterType == FilterType.NO_FILTER) {
|
||||
if (state.filters.mainFilter == MainFilter.ALL) {
|
||||
viewModel.openDialog(DialogState.ConfirmDialog)
|
||||
} else {
|
||||
viewModel.setAllItemsRead()
|
||||
@ -206,47 +288,78 @@ object TimelineTab : Tab {
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
SwipeRefresh(
|
||||
state = swipeState,
|
||||
onRefresh = { viewModel.refreshTimeline() },
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||
) {
|
||||
when {
|
||||
state.isRefreshing -> RefreshScreen(
|
||||
currentFeed = state.currentFeed,
|
||||
feedCount = state.feedCount,
|
||||
feedMax = state.feedMax
|
||||
)
|
||||
|
||||
items.isLoading() -> {
|
||||
Log.d("TAG", "loading")
|
||||
CenteredColumn {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
CenteredProgressIndicator()
|
||||
}
|
||||
|
||||
items.isError() -> Text(text = "error")
|
||||
else -> {
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing)
|
||||
) {
|
||||
items(
|
||||
count = items.itemCount,
|
||||
//key = { items[it]!! },
|
||||
contentType = { "item_with_feed" }
|
||||
) { itemCount ->
|
||||
val itemWithFeed = items[itemCount]!!
|
||||
items.isError() -> {
|
||||
Placeholder(
|
||||
text = stringResource(R.string.error_occured),
|
||||
painter = painterResource(id = R.drawable.ic_error)
|
||||
)
|
||||
}
|
||||
|
||||
TimelineItem(
|
||||
itemWithFeed = itemWithFeed,
|
||||
onClick = {
|
||||
viewModel.setItemRead(itemWithFeed.item)
|
||||
navigator.push(ItemScreen())
|
||||
},
|
||||
onFavorite = { viewModel.updateStarState(itemWithFeed.item) },
|
||||
onReadLater = {},
|
||||
onShare = {
|
||||
viewModel.shareItem(itemWithFeed.item, context)
|
||||
},
|
||||
compactLayout = true
|
||||
)
|
||||
else -> {
|
||||
if (items.itemCount > 0) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing)
|
||||
) {
|
||||
items(
|
||||
count = items.itemCount,
|
||||
key = items.itemKey { it.item.id },
|
||||
) { itemCount ->
|
||||
val itemWithFeed = items[itemCount]!!
|
||||
|
||||
TimelineItem(
|
||||
itemWithFeed = itemWithFeed,
|
||||
onClick = {
|
||||
viewModel.setItemRead(itemWithFeed.item)
|
||||
navigator.push(ItemScreen())
|
||||
},
|
||||
onFavorite = { viewModel.updateStarState(itemWithFeed.item) },
|
||||
onShare = {
|
||||
viewModel.shareItem(itemWithFeed.item, context)
|
||||
},
|
||||
compactLayout = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshContainer(
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
} else {
|
||||
// Empty lazyColumn to let the pull to refresh be usable
|
||||
// when the no item placeholder is displayed
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {}
|
||||
|
||||
PullToRefreshContainer(
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
|
||||
Placeholder(
|
||||
text = stringResource(R.string.no_item),
|
||||
painter = painterResource(R.drawable.ic_rss_feed_grey)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ fun DrawerFolderItem(
|
||||
) {
|
||||
val colors = NavigationDrawerItemDefaults.colors()
|
||||
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
var isExpanded by remember { mutableStateOf(feeds.any { it.id == selectedFeed }) }
|
||||
val rotationState by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 180f else 0f,
|
||||
label = "drawer item arrow rotation"
|
||||
@ -125,6 +125,7 @@ fun DrawerFolderItem(
|
||||
AsyncImage(
|
||||
model = feed.iconUrl,
|
||||
contentDescription = feed.name,
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
placeholder = painterResource(id = R.drawable.ic_folder_grey),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
@ -28,12 +28,12 @@ import com.readrops.app.compose.timelime.TimelineState
|
||||
import com.readrops.app.compose.util.theme.spacing
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.filters.FilterType
|
||||
import com.readrops.db.filters.MainFilter
|
||||
|
||||
@Composable
|
||||
fun TimelineDrawer(
|
||||
state: TimelineState,
|
||||
onClickDefaultItem: (FilterType) -> Unit,
|
||||
onClickDefaultItem: (MainFilter) -> Unit,
|
||||
onFolderClick: (Folder) -> Unit,
|
||||
onFeedClick: (Feed) -> Unit,
|
||||
) {
|
||||
@ -47,7 +47,7 @@ fun TimelineDrawer(
|
||||
Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing))
|
||||
|
||||
DrawerDefaultItems(
|
||||
selectedItem = state.filters.filterType,
|
||||
selectedItem = state.filters.mainFilter,
|
||||
onClick = { onClickDefaultItem(it) }
|
||||
)
|
||||
|
||||
@ -98,6 +98,7 @@ fun TimelineDrawer(
|
||||
AsyncImage(
|
||||
model = feed.iconUrl,
|
||||
contentDescription = feed.name,
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
placeholder = painterResource(id = R.drawable.ic_folder_grey),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
@ -116,8 +117,8 @@ fun TimelineDrawer(
|
||||
|
||||
@Composable
|
||||
fun DrawerDefaultItems(
|
||||
selectedItem: FilterType,
|
||||
onClick: (FilterType) -> Unit,
|
||||
selectedItem: MainFilter,
|
||||
onClick: (MainFilter) -> Unit,
|
||||
) {
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = stringResource(R.string.articles)) },
|
||||
@ -127,8 +128,8 @@ fun DrawerDefaultItems(
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selected = selectedItem == FilterType.NO_FILTER,
|
||||
onClick = { onClick(FilterType.NO_FILTER) },
|
||||
selected = selectedItem == MainFilter.ALL,
|
||||
onClick = { onClick(MainFilter.ALL) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
|
||||
@ -140,8 +141,8 @@ fun DrawerDefaultItems(
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selected = selectedItem == FilterType.NEW,
|
||||
onClick = { onClick(FilterType.NEW) },
|
||||
selected = selectedItem == MainFilter.NEW,
|
||||
onClick = { onClick(MainFilter.NEW) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
|
||||
@ -153,21 +154,8 @@ fun DrawerDefaultItems(
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selected = selectedItem == FilterType.STARS_FILTER,
|
||||
onClick = { onClick(FilterType.STARS_FILTER) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = stringResource(R.string.read_later)) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_read_later),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selected = selectedItem == FilterType.READ_IT_LATER_FILTER,
|
||||
onClick = { onClick(FilterType.READ_IT_LATER_FILTER) },
|
||||
selected = selectedItem == MainFilter.STARS,
|
||||
onClick = { onClick(MainFilter.STARS) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package com.readrops.app.compose.util.components
|
||||
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
|
||||
abstract class AndroidScreen : Screen {
|
||||
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
}
|
@ -25,13 +25,15 @@ fun BaseDialog(
|
||||
title: String,
|
||||
icon: Painter,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
|
@ -0,0 +1,25 @@
|
||||
package com.readrops.app.compose.util.components
|
||||
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.readrops.app.compose.util.theme.VeryShortSpacer
|
||||
|
||||
@Composable
|
||||
fun RefreshScreen(
|
||||
currentFeed: String,
|
||||
feedCount: Int,
|
||||
feedMax: Int
|
||||
) {
|
||||
CenteredColumn {
|
||||
LinearProgressIndicator(
|
||||
progress = { feedCount.toFloat() / feedMax.toFloat() }
|
||||
)
|
||||
|
||||
VeryShortSpacer()
|
||||
|
||||
Text(
|
||||
text = "$currentFeed ($feedCount/$feedMax)"
|
||||
)
|
||||
}
|
||||
}
|
@ -142,5 +142,18 @@
|
||||
<string name="hide_feeds">Cacher les flux sans nouveaux items</string>
|
||||
<string name="mark_items_read">Marquer les items comme lus pendant le défilement</string>
|
||||
<string name="filters">Filtres</string>
|
||||
|
||||
<plurals name="error_occurred">
|
||||
<item quantity="one">Une erreur s\'est produite</item>
|
||||
<item quantity="other">Des erreurs se sont produites</item>
|
||||
</plurals>
|
||||
<string name="details">Détails</string>
|
||||
<string name="synchronization_errors">Erreurs de synchronisation</string>
|
||||
<plurals name="error_occurred_feed">
|
||||
<item quantity="one">Une erreur s\'est produite pour le flux suivant :</item>
|
||||
<item quantity="other">Des erreurs se sont produites pour les flux suivants :</item>
|
||||
</plurals>
|
||||
<string name="unreachable_feed_http_error">Flux non attaignable, erreur HTTP %1$s</string>
|
||||
<string name="network_failure">Erreur réseau: %1$s</string>
|
||||
<string name="processing_feed_error">Erreur de traitement du flux</string>
|
||||
<string name="unreachable_feed">Flux non attaignable</string>
|
||||
</resources>
|
@ -148,4 +148,18 @@
|
||||
<string name="mark_items_read">Mark items read on scroll</string>
|
||||
<string name="new_articles">New articles</string>
|
||||
<string name="filters">Filters</string>
|
||||
<plurals name="error_occurred">
|
||||
<item quantity="one">An error occurred</item>
|
||||
<item quantity="other">Some errors occurred</item>
|
||||
</plurals>
|
||||
<string name="details">Details</string>
|
||||
<string name="synchronization_errors">Synchronization errors</string>
|
||||
<plurals name="error_occurred_feed">
|
||||
<item quantity="one">An error occurred for the following feed:</item>
|
||||
<item quantity="other">Some errors occurred for the following feeds:</item>
|
||||
</plurals>
|
||||
<string name="unreachable_feed_http_error">Unreachable feed, HTTP error %1$s</string>
|
||||
<string name="network_failure">Network failure: %1$s</string>
|
||||
<string name="processing_feed_error">Processing feed error</string>
|
||||
<string name="unreachable_feed">Unreachable feed</string>
|
||||
</resources>
|
@ -76,13 +76,9 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
def room_version = "2.4.3"
|
||||
api "androidx.room:room-runtime:$room_version"
|
||||
api "androidx.room:room-ktx:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-rxjava2:$room_version"
|
||||
androidTestImplementation "androidx.room:room-testing:$room_version"
|
||||
implementation "androidx.room:room-paging:$room_version"
|
||||
implementation(libs.bundles.room)
|
||||
kapt(libs.room.compiler)
|
||||
androidTestImplementation(libs.room.testing)
|
||||
|
||||
implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4'
|
||||
kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4'
|
||||
@ -92,10 +88,8 @@ dependencies {
|
||||
|
||||
api 'joda-time:joda-time:2.10.10'
|
||||
|
||||
api "io.insert-koin:koin-core:$rootProject.ext.koin_version"
|
||||
api "io.insert-koin:koin-android:$rootProject.ext.koin_version"
|
||||
api "io.insert-koin:koin-androidx-compose:3.4.2"
|
||||
api "io.insert-koin:koin-android-compat:$rootProject.ext.koin_version"
|
||||
implementation(libs.bundles.koin)
|
||||
testImplementation(libs.bundles.kointest)
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
|
@ -4,8 +4,9 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.readrops.db.filters.FilterType
|
||||
import com.readrops.db.filters.ListSortType
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import com.readrops.db.filters.SubFilter
|
||||
import com.readrops.db.queries.ItemsQueryBuilder
|
||||
import com.readrops.db.queries.QueryFilters
|
||||
import junit.framework.TestCase.assertFalse
|
||||
@ -51,7 +52,7 @@ class ItemsQueryBuilderTest {
|
||||
|
||||
@Test
|
||||
fun feedFilterCaseTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER,
|
||||
val queryFilters = QueryFilters(accountId = 1, subFilter = SubFilter.FEED,
|
||||
filterFeedId = 15)
|
||||
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
|
||||
@ -60,19 +61,9 @@ class ItemsQueryBuilderTest {
|
||||
assertTrue(query.sql.contains("feed_id = 15 And read_it_later = 0"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readLaterFilterCaseTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.READ_IT_LATER_FILTER)
|
||||
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
|
||||
database.query(query)
|
||||
|
||||
assertTrue(query.sql.contains("read_it_later = 1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun starsFilterCaseTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.STARS_FILTER)
|
||||
val queryFilters = QueryFilters(accountId = 1, mainFilter = MainFilter.STARS)
|
||||
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
|
||||
database.query(query)
|
||||
@ -82,7 +73,7 @@ class ItemsQueryBuilderTest {
|
||||
|
||||
@Test
|
||||
fun folderFilterCaseTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FOLDER_FILER, filterFolderId = 1)
|
||||
val queryFilters = QueryFilters(accountId = 1, subFilter = SubFilter.FOLDER, filterFolderId = 1)
|
||||
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
|
||||
database.query(query)
|
||||
@ -107,7 +98,7 @@ class ItemsQueryBuilderTest {
|
||||
|
||||
@Test
|
||||
fun separateStateTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, showReadItems = false, filterType = FilterType.STARS_FILTER)
|
||||
val queryFilters = QueryFilters(accountId = 1, showReadItems = false, mainFilter = MainFilter.STARS)
|
||||
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters, true)
|
||||
database.query(query)
|
||||
@ -127,7 +118,7 @@ class ItemsQueryBuilderTest {
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun filterFeedIdExceptionTest() {
|
||||
val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER)
|
||||
val queryFilters = QueryFilters(accountId = 1, subFilter = SubFilter.FEED)
|
||||
ItemsQueryBuilder.buildItemsQuery(queryFilters)
|
||||
}
|
||||
}
|
@ -17,17 +17,17 @@ interface BaseDao<T> {
|
||||
fun compatInsert(entity: T): Long
|
||||
|
||||
@Insert
|
||||
fun insert(entities: List<T>?): List<Long>
|
||||
fun insert(entities: List<T>): List<Long>
|
||||
|
||||
@Update
|
||||
fun update(entity: T): Completable
|
||||
|
||||
@Update
|
||||
fun update(entities: List<T>?): Completable
|
||||
fun update(entities: List<T>): Completable
|
||||
|
||||
@Delete
|
||||
fun delete(entity: T): Completable
|
||||
|
||||
@Delete
|
||||
fun delete(entities: List<T>?): Completable
|
||||
fun delete(entities: List<T>): Completable
|
||||
}
|
@ -10,17 +10,17 @@ interface NewBaseDao<T> {
|
||||
suspend fun insert(entity: T): Long
|
||||
|
||||
@Insert
|
||||
suspend fun insert(entities: List<T>?): List<Long>
|
||||
suspend fun insert(entities: List<T>): List<Long>
|
||||
|
||||
@Update
|
||||
suspend fun update(entity: T)
|
||||
|
||||
@Update
|
||||
suspend fun update(entities: List<T>?)
|
||||
suspend fun update(entities: List<T>)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(entity: T)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(entities: List<T>?)
|
||||
suspend fun delete(entities: List<T>)
|
||||
}
|
@ -2,15 +2,21 @@ 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.Item
|
||||
import com.readrops.db.pojo.FeedWithCount
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class NewFeedDao : NewBaseDao<Feed> {
|
||||
|
||||
@Query("Select * From Feed")
|
||||
abstract fun selectFeeds(): Flow<List<Feed>>
|
||||
@Query("Select * From Feed Where id = :feedId")
|
||||
abstract suspend fun selectFeed(feedId: Int): Feed
|
||||
|
||||
@Query("Select * From Feed Where folder_id = :folderId")
|
||||
abstract suspend fun selectFeedsByFolder(folderId: Int): List<Feed>
|
||||
|
||||
@Query("Select * from Feed Where account_id = :accountId order by name ASC")
|
||||
abstract suspend fun selectFeeds(accountId: Int): List<Feed>
|
||||
@ -21,11 +27,12 @@ abstract class NewFeedDao : NewBaseDao<Feed> {
|
||||
@Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end")
|
||||
abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean
|
||||
|
||||
@Query("Select Feed.*, count(*) as unreadCount From Feed Left Join Item On Feed.id = Item.feed_id " +
|
||||
"Where Feed.folder_id is Null And (Item.read = 0 OR Item.read is NULL) And Feed.account_id = :accountId Group by Feed.id")
|
||||
abstract fun selectFeedsWithoutFolder(accountId: Int): Flow<List<FeedWithCount>>
|
||||
@RawQuery(observedEntities = [Feed::class, Item::class])
|
||||
abstract fun selectFeedsWithoutFolder(query: SupportSQLiteQuery): Flow<List<FeedWithCount>>
|
||||
|
||||
@Query("Update Feed set name = :feedName, url = :feedUrl, folder_id = :folderId Where id = :feedId")
|
||||
abstract fun updateFeedFields(feedId: Int, feedName: String, feedUrl: String, folderId: Int?)
|
||||
|
||||
@Query("Select count(*) from Feed Where account_id = :accountId")
|
||||
abstract suspend fun selectFeedCount(accountId: Int): Int
|
||||
}
|
@ -2,20 +2,20 @@ 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
|
||||
abstract class NewFolderDao : NewBaseDao<Folder> {
|
||||
interface NewFolderDao : NewBaseDao<Folder> {
|
||||
|
||||
@Query("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, count(*) As unreadCount, Folder.account_id as accountId " +
|
||||
"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 (Item.read = 0 OR Item.read is NULL) " +
|
||||
"And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id")
|
||||
abstract fun selectFoldersAndFeeds(accountId: Int): Flow<List<FolderWithFeed>>
|
||||
@RawQuery(observedEntities = [Folder::class, Feed::class, Item::class])
|
||||
fun selectFoldersAndFeeds(query: SupportSQLiteQuery): Flow<List<FolderWithFeed>>
|
||||
|
||||
@Query("Select * From Folder Where account_id = :accountId")
|
||||
abstract fun selectFolders(accountId: Int): Flow<List<Folder>>
|
||||
fun selectFolders(accountId: Int): Flow<List<Folder>>
|
||||
}
|
@ -29,6 +29,11 @@ abstract class NewItemDao : NewBaseDao<Item> {
|
||||
@Query("Update Item set read = 1 Where starred = 1 And feed_id IN (Select id From Feed Where account_id = :accountId)")
|
||||
abstract suspend fun setAllStarredItemsRead(accountId: Int)
|
||||
|
||||
@Query("Update Item set read = 1 Where DateTime(Round(pub_date / 1000), 'unixepoch') " +
|
||||
"Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\") " +
|
||||
"And feed_id IN (Select id From Feed Where account_id = :accountId)")
|
||||
abstract suspend fun setAllNewItemsRead(accountId: Int)
|
||||
|
||||
@Query("Update Item set read = 1 Where feed_id IN " +
|
||||
"(Select id From Feed Where id = :feedId And account_id = :accountId)")
|
||||
abstract suspend fun setAllItemsReadByFeed(feedId: Int, accountId: Int)
|
||||
|
@ -1,10 +0,0 @@
|
||||
package com.readrops.db.filters;
|
||||
|
||||
enum class FilterType {
|
||||
FEED_FILTER,
|
||||
FOLDER_FILER,
|
||||
READ_IT_LATER_FILTER,
|
||||
STARS_FILTER,
|
||||
NO_FILTER,
|
||||
NEW
|
||||
}
|
13
db/src/main/java/com/readrops/db/filters/Filters.kt
Normal file
13
db/src/main/java/com/readrops/db/filters/Filters.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package com.readrops.db.filters
|
||||
|
||||
enum class MainFilter {
|
||||
STARS,
|
||||
NEW,
|
||||
ALL
|
||||
}
|
||||
|
||||
enum class SubFilter {
|
||||
FEED,
|
||||
FOLDER,
|
||||
ALL
|
||||
}
|
@ -25,6 +25,11 @@ data class FolderWithFeed(
|
||||
)
|
||||
|
||||
data class FeedWithCount(
|
||||
@Embedded val feed: Feed,
|
||||
val unreadCount: Int
|
||||
val feedId: Int = 0,
|
||||
val feedName: String? = null,
|
||||
val feedIcon: String? = null,
|
||||
val feedUrl: String? = null,
|
||||
val feedSiteUrl: String? = null,
|
||||
val unreadCount: Int = 0,
|
||||
val accountId: Int = 0
|
||||
)
|
@ -0,0 +1,51 @@
|
||||
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,
|
||||
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,
|
||||
(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.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,
|
||||
(Select count(*) From main Where (itemRead = 0)) as unreadCount From main Group by feedId Order By feedName
|
||||
""".trimIndent())
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
}
|
@ -2,90 +2,121 @@ package com.readrops.db.queries
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
import com.readrops.db.filters.FilterType
|
||||
import com.readrops.db.filters.ListSortType
|
||||
import com.readrops.db.filters.MainFilter
|
||||
import com.readrops.db.filters.SubFilter
|
||||
|
||||
object ItemsQueryBuilder {
|
||||
|
||||
private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "clean_description", "image_link", "pub_date", "link",
|
||||
"read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time",
|
||||
"Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name")
|
||||
private val COLUMNS = arrayOf(
|
||||
"Item.id",
|
||||
"Item.remoteId",
|
||||
"title",
|
||||
"clean_description",
|
||||
"image_link",
|
||||
"pub_date",
|
||||
"link",
|
||||
"read_it_later",
|
||||
"Feed.name",
|
||||
"text_color",
|
||||
"background_color",
|
||||
"icon_url",
|
||||
"read_time",
|
||||
"Feed.id as feedId",
|
||||
"Feed.account_id",
|
||||
"Folder.id as folder_id",
|
||||
"Folder.name as folder_name"
|
||||
)
|
||||
|
||||
private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read",
|
||||
"case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred")
|
||||
private val SEPARATE_STATE_COLUMNS = arrayOf(
|
||||
"case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read",
|
||||
"case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred"
|
||||
)
|
||||
|
||||
private val OTHER_COLUMNS = arrayOf("read", "starred")
|
||||
|
||||
private val SELECT_ALL_JOIN = """Item INNER JOIN Feed on Item.feed_id = Feed.id
|
||||
LEFT JOIN Folder on Feed.folder_id = Folder.id """.trimIndent()
|
||||
|
||||
private const val SEPARATE_STATE_JOIN = "LEFT JOIN ItemState On Item.remoteId = ItemState.remote_id"
|
||||
private const val SEPARATE_STATE_JOIN =
|
||||
"LEFT JOIN ItemState On Item.remoteId = ItemState.remote_id"
|
||||
|
||||
private const val ORDER_BY_ASC = "pub_date DESC"
|
||||
|
||||
private const val ORDER_BY_DESC = "pub_date ASC"
|
||||
|
||||
@JvmStatic
|
||||
fun buildItemsQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery =
|
||||
buildQuery(queryFilters, separateState)
|
||||
buildQuery(queryFilters, separateState)
|
||||
|
||||
@JvmStatic
|
||||
fun buildItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery =
|
||||
buildQuery(queryFilters, false)
|
||||
buildQuery(queryFilters, false)
|
||||
|
||||
private fun buildQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery = with(queryFilters) {
|
||||
if (accountId == 0)
|
||||
throw IllegalArgumentException("AccountId must be greater than 0")
|
||||
private fun buildQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery =
|
||||
with(queryFilters) {
|
||||
if (accountId == 0)
|
||||
throw IllegalArgumentException("AccountId must be greater than 0")
|
||||
|
||||
if (filterType == FilterType.FEED_FILTER && filterFeedId == 0)
|
||||
throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER")
|
||||
if (queryFilters.subFilter == SubFilter.FEED && filterFeedId == 0)
|
||||
throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER")
|
||||
|
||||
val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus(OTHER_COLUMNS)
|
||||
val selectAllJoin = if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN
|
||||
|
||||
SupportSQLiteQueryBuilder.builder(selectAllJoin).run {
|
||||
columns(columns)
|
||||
selection(buildWhereClause(this@with, separateState), null)
|
||||
orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC)
|
||||
|
||||
create()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildWhereClause(queryFilters: QueryFilters, separateState: Boolean): String = StringBuilder(500).run {
|
||||
append("Feed.account_id = ${queryFilters.accountId} And ")
|
||||
|
||||
if (!queryFilters.showReadItems) {
|
||||
if (separateState)
|
||||
append("ItemState.read = 0 And ")
|
||||
val columns = if (separateState)
|
||||
COLUMNS.plus(SEPARATE_STATE_COLUMNS)
|
||||
else
|
||||
append("Item.read = 0 And ")
|
||||
}
|
||||
COLUMNS.plus(OTHER_COLUMNS)
|
||||
|
||||
when (queryFilters.filterType) {
|
||||
FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0")
|
||||
FilterType.FOLDER_FILER -> append("folder_id = ${queryFilters.filterFolderId} And read_it_later = 0")
|
||||
FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1")
|
||||
FilterType.STARS_FILTER -> {
|
||||
if (separateState) {
|
||||
append("ItemState.starred = 1 And read_it_later = 0")
|
||||
} else {
|
||||
append("starred = 1 And read_it_later = 0")
|
||||
}
|
||||
val selectAllJoin =
|
||||
if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN
|
||||
|
||||
SupportSQLiteQueryBuilder.builder(selectAllJoin).run {
|
||||
columns(columns)
|
||||
selection(buildWhereClause(this@with, separateState), null)
|
||||
orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC)
|
||||
|
||||
create()
|
||||
}
|
||||
else -> append("read_it_later = 0")
|
||||
}
|
||||
|
||||
toString()
|
||||
}
|
||||
private fun buildWhereClause(queryFilters: QueryFilters, separateState: Boolean): String =
|
||||
StringBuilder(500).run {
|
||||
append("Feed.account_id = ${queryFilters.accountId} And ")
|
||||
|
||||
if (!queryFilters.showReadItems) {
|
||||
if (separateState)
|
||||
append("ItemState.read = 0 And ")
|
||||
else
|
||||
append("Item.read = 0 And ")
|
||||
}
|
||||
|
||||
when (queryFilters.mainFilter) {
|
||||
MainFilter.STARS -> {
|
||||
if (separateState) {
|
||||
append("ItemState.starred = 1 And read_it_later = 0 ")
|
||||
} else {
|
||||
append("starred = 1 And read_it_later = 0 ")
|
||||
}
|
||||
}
|
||||
|
||||
MainFilter.NEW -> append("DateTime(Round(pub_date / 1000), 'unixepoch') Between DateTime(DateTime(\"now\"), \"-24 hour\") And DateTime(\"now\") ")
|
||||
else -> append("read_it_later = 0 ")
|
||||
}
|
||||
|
||||
when (queryFilters.subFilter) {
|
||||
SubFilter.FEED -> append("And feed_id = ${queryFilters.filterFeedId} And read_it_later = 0")
|
||||
SubFilter.FOLDER -> append("And folder_id = ${queryFilters.filterFolderId} And read_it_later = 0")
|
||||
else -> {}
|
||||
}
|
||||
|
||||
toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class QueryFilters(
|
||||
var showReadItems: Boolean = true,
|
||||
var filterFeedId: Int = 0,
|
||||
var filterFolderId: Int = 0,
|
||||
var accountId: Int = 0,
|
||||
var filterType: FilterType = FilterType.NO_FILTER,
|
||||
var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST,
|
||||
val showReadItems: Boolean = true,
|
||||
val filterFeedId: Int = 0,
|
||||
val filterFolderId: Int = 0,
|
||||
val accountId: Int = 0,
|
||||
val mainFilter: MainFilter = MainFilter.ALL,
|
||||
val subFilter: SubFilter = SubFilter.ALL,
|
||||
val sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST,
|
||||
)
|
65
gradle/libs.versions.toml
Normal file
65
gradle/libs.versions.toml
Normal file
@ -0,0 +1,65 @@
|
||||
[versions]
|
||||
compose-bom = "2024.02.02"
|
||||
voyager = "1.0.0"
|
||||
lifecycle = "2.7.0"
|
||||
coil = "2.4.0"
|
||||
coroutines = "1.8.0"
|
||||
room = "2.6.1"
|
||||
koin-bom = "3.5.0"
|
||||
|
||||
[libraries]
|
||||
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
compose-foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||
compose-animation = { module = "androidx.compose.animation:animation" }
|
||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
# specify material3 version is required for gradle to find the dependency
|
||||
compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" }
|
||||
compose-activity = "androidx.activity:activity-compose:1.8.2"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
|
||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
|
||||
lifecyle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
|
||||
coil-core = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
|
||||
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "room" } #TODO remove usage of this
|
||||
room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
|
||||
room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
|
||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||
koin-core = { module = "io.insert-koin:koin-core" }
|
||||
koin-android = { module = "io.insert-koin:koin-android" }
|
||||
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" }
|
||||
koin-android-compat = { module = "io.insert-koin:koin-android-compat" } #TODO remove usage of this
|
||||
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin-bom" }
|
||||
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin-bom" }
|
||||
|
||||
[bundles]
|
||||
compose = ["bom", "compose-foundation", "compose-runtime", "compose-animation",
|
||||
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3"]
|
||||
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-koin", "voyager-transitions"]
|
||||
lifecycle = ["lifecycle-viewmodel-ktx", "lifecycle-viewmodel-compose", "lifecycle-viewmodel-savedstate",
|
||||
"lifecyle-runtime-compose"]
|
||||
coil = ["coil-core", "coil-compose"]
|
||||
coroutines = ["coroutines-core", "coroutines-android"]
|
||||
room = ["room-runtime", "room-ktx", "room-rxjava2", "room-paging"]
|
||||
koin = ["koin-bom", "koin-core", "koin-android", "koin-androidx-compose", "koin-android-compat"]
|
||||
kointest = ["koin-test", "koin-test-junit4"]
|
Loading…
x
Reference in New Issue
Block a user