From 6e9bde60c103346d448e64e2d03a3dbdcc694dea Mon Sep 17 00:00:00 2001 From: ROGER Mathieu Date: Tue, 30 Jul 2024 19:37:45 +0200 Subject: [PATCH 1/4] chore(ci) : enable CI --- .github/dependabot.yml | 13 ++++++ .github/workflows/develop-release-ci.yml | 52 ++++++++++++++++++++++ .github/workflows/master-release-ci.yml | 55 ++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/develop-release-ci.yml create mode 100644 .github/workflows/master-release-ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..df36f73 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gradle" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + open-pull-requests-limit: 3 + target-branch: "main" diff --git a/.github/workflows/develop-release-ci.yml b/.github/workflows/develop-release-ci.yml new file mode 100644 index 0000000..dc70d80 --- /dev/null +++ b/.github/workflows/develop-release-ci.yml @@ -0,0 +1,52 @@ +name: Develop Release CI + +on: + push: + branches: + - 'develop' + pull_request: + branches: + - 'master' + - 'release*' + - 'develop' + +jobs: + test-and-build-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up Ruby for fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.6' + - name: set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + - name: Configure Keystore + run: | + echo "$ANDROID_KEYSTORE_FILE" > keystore.jks.b64 + base64 -d -i keystore.jks.b64 > app/keystore.jks + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + env: + ANDROID_KEYSTORE_FILE: ${{ secrets.ANDROID_KEYSTORE_FILE }} + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + # Step 3: Check the code with ktlint, you can remove this job if you don't use ktlint + - name: Run Kotlin Linter + run: ./gradlew ktlintCheck + + # Step 3: Check the code with Android linter + - name: Run Android Linter + run: ./gradlew lintDebug + - name: Install bundle + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Attempt to test and build app throught fastlane + run: bundle exec fastlane test \ No newline at end of file diff --git a/.github/workflows/master-release-ci.yml b/.github/workflows/master-release-ci.yml new file mode 100644 index 0000000..1207add --- /dev/null +++ b/.github/workflows/master-release-ci.yml @@ -0,0 +1,55 @@ +name: Master Release CI + +on: + push: + branches: + - 'master' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: set up Ruby for fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.6' + - name: set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + - name: Configure Keystore + run: | + echo "$ANDROID_KEYSTORE_FILE" > keystore.jks.b64 + base64 -d -i keystore.jks.b64 > app/keystore.jks + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + env: + ANDROID_KEYSTORE_FILE: ${{ secrets.ANDROID_KEYSTORE_FILE }} + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + # Step 3: Check the code with ktlint, you can remove this job if you don't use ktlint + - name: Run Kotlin Linter + run: ./gradlew ktlintCheck + + # Step 3: Check the code with Android linter + - name: Run Android Linter + run: ./gradlew lintDebug + - name: Create Google Play Config file + run: | + echo "$PLAY_CONFIG_JSON" > play_config.json.b64 + base64 -d -i play_config.json.b64 > api-service-account.json + env: + PLAY_CONFIG_JSON: ${{ secrets.PLAY_CONFIG_JSON }} + - name: Install bundle + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Attempt to test and build app throught fastlane + run: bundle exec fastlane test + - name: Distribute app to Alpha track 🚀 + run: bundle exec fastlane alpha \ No newline at end of file From d99ad185b5634e3508cf7e9ebe7f03cf9d3b6558 Mon Sep 17 00:00:00 2001 From: ROGER Mathieu Date: Tue, 30 Jul 2024 20:44:04 +0200 Subject: [PATCH 2/4] chore(ci): enable ktlint --- app/build.gradle.kts | 37 +++++++++++++++++++++---------------- build.gradle.kts | 6 ++---- settings.gradle.kts | 1 - 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b9e798d..11a8188 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,12 @@ plugins { id("kotlin-kapt") id("com.google.dagger.hilt.android") id("kotlin-parcelize") + id("org.jlleitschuh.gradle.ktlint") +} + +ktlint { + android.set(true) + outputColorName.set("RED") } android { @@ -28,7 +34,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -68,40 +74,39 @@ dependencies { androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) //Splash Api + debugImplementation(libs.androidx.ui.test.manifest) // Splash Api implementation(libs.androidx.core.splashscreen) - //Compose Navigation + // Compose Navigation implementation("androidx.navigation:navigation-compose:2.7.7") - //Dagger Hilt + // Dagger Hilt implementation("com.google.dagger:hilt-android:2.51.1") kapt("com.google.dagger:hilt-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") - - //Retrofit + // Retrofit implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.retrofit2:converter-gson:2.11.0") - //Coil + // Coil implementation("io.coil-kt:coil-compose:2.6.0") - //Datastore - implementation ("androidx.datastore:datastore-preferences:1.1.1") + // Datastore + implementation("androidx.datastore:datastore-preferences:1.1.1") - //Compose Foundation - implementation (libs.androidx.foundation) + // Compose Foundation + implementation(libs.androidx.foundation) - //Accompanist - implementation ("com.google.accompanist:accompanist-systemuicontroller:0.31.4-beta") + // Accompanist + implementation("com.google.accompanist:accompanist-systemuicontroller:0.31.4-beta") - //Paging 3 + // Paging 3 implementation("androidx.paging:paging-runtime:3.3.0") implementation("androidx.paging:paging-compose:3.3.0") - //Room + // Room implementation("androidx.room:room-runtime:2.6.1") kapt("androidx.room:room-compiler:2.6.1") implementation("androidx.room:room-ktx:2.6.1") -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index d5afcbb..111f014 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,14 +3,12 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false id("com.google.dagger.hilt.android") version "2.44" apply false + id("org.jlleitschuh.gradle.ktlint") version "12.1.1" } - - buildscript { dependencies { classpath("com.google.dagger:hilt-android-gradle-plugin:2.51.1") } - -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 33c0889..eb05ad8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,3 @@ dependencyResolutionManagement { rootProject.name = "DroidStoryBox" include(":app") - \ No newline at end of file From a99590d9984be986751b8cdc80f3d3d567b50ee9 Mon Sep 17 00:00:00 2001 From: ROGER Mathieu Date: Tue, 30 Jul 2024 20:50:24 +0200 Subject: [PATCH 3/4] chore(ci): run ktlint format --- app/build.gradle.kts | 1 + .../droidstorybox/ExampleInstrumentedTest.kt | 8 +- .../info/octera/droidstorybox/MainActivity.kt | 7 +- .../octera/droidstorybox/MainApplication.kt | 3 +- .../octera/droidstorybox/MainViewModel.kt | 41 +++--- .../droidstorybox/data/local/NewsDao.kt | 6 +- .../droidstorybox/data/local/NewsDatabase.kt | 6 +- .../data/local/NewsTypeConverter.kt | 9 +- .../data/manager/LocalUserManagerImpl.kt | 11 +- .../droidstorybox/data/remote/NewsApi.kt | 7 +- .../data/remote/NewsPagingSource.kt | 22 ++- .../data/remote/SearchNewsPagingSource.kt | 22 ++- .../data/remote/dto/NewsResponse.kt | 4 +- .../data/repository/NewsRepositoryImpl.kt | 20 +-- .../info/octera/droidstorybox/di/AppModule.kt | 52 +++---- .../domain/manager/LocalUserManager.kt | 3 +- .../droidstorybox/domain/model/Article.kt | 4 +- .../droidstorybox/domain/model/Source.kt | 5 +- .../domain/repository/NewsRepository.kt | 8 +- .../usecases/app_entry/AppEntryUseCases.kt | 4 +- .../domain/usecases/app_entry/ReadAppEntry.kt | 9 +- .../domain/usecases/app_entry/SaveAppEntry.kt | 7 +- .../domain/usecases/news/DeleteArticle.kt | 10 +- .../domain/usecases/news/GetNews.kt | 6 +- .../domain/usecases/news/NewsUseCases.kt | 4 +- .../domain/usecases/news/SearchNews.kt | 11 +- .../domain/usecases/news/SelectArticle.kt | 8 +- .../domain/usecases/news/SelectArticles.kt | 6 +- .../domain/usecases/news/UpsertArticle.kt | 8 +- .../droidstorybox/presentation/Dimens.kt | 4 +- .../presentation/bookmark/BookmarkScreen.kt | 26 ++-- .../presentation/bookmark/BookmarkState.kt | 3 +- .../bookmark/BookmarkViewModel.kt | 32 ++--- .../presentation/common/ArticleCard.kt | 55 ++++---- .../presentation/common/ArticlesList.kt | 71 +++------- .../presentation/common/EmptyScreen.kt | 32 +++-- .../presentation/common/NewsButton.kt | 42 +++--- .../presentation/common/SearchBar.kt | 84 ++++++------ .../presentation/common/ShimmerEffects.kt | 72 +++++----- .../presentation/details/DetailViewModel.kt | 60 ++++---- .../presentation/details/DetailsEvent.kt | 4 +- .../presentation/details/DetailsScreen.kt | 87 ++++++------ .../details/components/DetailsTopBar.kt | 31 +++-- .../presentation/home/HomeScreen.kt | 49 +++---- .../presentation/home/HomeViewModel.kt | 20 +-- .../presentation/navgraph/NavGraph.kt | 31 +---- .../presentation/navgraph/Route.kt | 4 +- .../news_navigation/NewsNavigator.kt | 128 +++++++++--------- .../components/NewsBottomNavigation.kt | 40 +++--- .../onboarding/OnBoardingEvent.kt | 6 +- .../onboarding/OnBoardingViewModel.kt | 31 +++-- .../presentation/onboarding/Page.kt | 40 +++--- .../onboarding/components/OnBoardingPage.kt | 21 +-- .../onboarding/components/OnBoardingScreen.kt | 49 +++---- .../onboarding/components/PageIndicator.kt | 21 +-- .../presentation/search/SearchEvent.kt | 4 +- .../presentation/search/SearchScreen.kt | 24 ++-- .../presentation/search/SearchState.kt | 5 +- .../presentation/search/SearchViewModel.kt | 46 ++++--- .../octera/droidstorybox/ui/theme/Color.kt | 11 +- .../octera/droidstorybox/ui/theme/Theme.kt | 52 +++---- .../octera/droidstorybox/ui/theme/Type.kt | 22 +-- .../octera/droidstorybox/util/Constants.kt | 2 +- 63 files changed, 726 insertions(+), 795 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11a8188..effb625 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { ktlint { android.set(true) outputColorName.set("RED") + ignoreFailures.set(true) } android { diff --git a/app/src/androidTest/java/info/octera/droidstorybox/ExampleInstrumentedTest.kt b/app/src/androidTest/java/info/octera/droidstorybox/ExampleInstrumentedTest.kt index da0c71a..ca47481 100644 --- a/app/src/androidTest/java/info/octera/droidstorybox/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/info/octera/droidstorybox/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package info.octera.droidstorybox -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.example.newsapp", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/MainActivity.kt b/app/src/main/java/info/octera/droidstorybox/MainActivity.kt index 5561c43..36b7073 100644 --- a/app/src/main/java/info/octera/droidstorybox/MainActivity.kt +++ b/app/src/main/java/info/octera/droidstorybox/MainActivity.kt @@ -10,9 +10,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import dagger.hilt.android.AndroidEntryPoint import info.octera.droidstorybox.presentation.navgraph.NavGraph import info.octera.droidstorybox.ui.theme.NewsAppTheme -import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -22,17 +22,16 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() - installSplashScreen().apply { setKeepOnScreenCondition { viewModel.splashCondition } } - actionBar?.hide(); + actionBar?.hide() setContent { NewsAppTheme { Box( - modifier = Modifier.background(MaterialTheme.colorScheme.background) + modifier = Modifier.background(MaterialTheme.colorScheme.background), ) { val startDestination = viewModel.startDestination NavGraph(startDestination = startDestination) diff --git a/app/src/main/java/info/octera/droidstorybox/MainApplication.kt b/app/src/main/java/info/octera/droidstorybox/MainApplication.kt index 720eb8a..4838364 100644 --- a/app/src/main/java/info/octera/droidstorybox/MainApplication.kt +++ b/app/src/main/java/info/octera/droidstorybox/MainApplication.kt @@ -4,5 +4,4 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class MainApplication : Application() { -} \ No newline at end of file +class MainApplication : Application() diff --git a/app/src/main/java/info/octera/droidstorybox/MainViewModel.kt b/app/src/main/java/info/octera/droidstorybox/MainViewModel.kt index c97749e..6f8b071 100644 --- a/app/src/main/java/info/octera/droidstorybox/MainViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/MainViewModel.kt @@ -5,35 +5,36 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import info.octera.droidstorybox.domain.usecases.app_entry.AppEntryUseCases import info.octera.droidstorybox.presentation.navgraph.Route -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( - private val appEntryUseCases: AppEntryUseCases -) : +class MainViewModel + @Inject + constructor( + private val appEntryUseCases: AppEntryUseCases, + ) : ViewModel() { + var splashCondition by mutableStateOf(true) + private set - var splashCondition by mutableStateOf(true) - private set - - var startDestination by mutableStateOf(Route.AppStartNavigation.route) - private set + var startDestination by mutableStateOf(Route.AppStartNavigation.route) + private set - init { - appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> - if (shouldStartFromHomeScreen) { - startDestination = Route.NewsNavigation.route - } else { - startDestination = Route.AppStartNavigation.route - } - delay(300) - splashCondition = false - }.launchIn(viewModelScope) + init { + appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> + if (shouldStartFromHomeScreen) { + startDestination = Route.NewsNavigation.route + } else { + startDestination = Route.AppStartNavigation.route + } + delay(300) + splashCondition = false + }.launchIn(viewModelScope) + } } -} \ No newline at end of file diff --git a/app/src/main/java/info/octera/droidstorybox/data/local/NewsDao.kt b/app/src/main/java/info/octera/droidstorybox/data/local/NewsDao.kt index 55bb3df..a6fce7e 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/local/NewsDao.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/local/NewsDao.kt @@ -8,10 +8,8 @@ import androidx.room.Query import info.octera.droidstorybox.domain.model.Article import kotlinx.coroutines.flow.Flow - @Dao interface NewsDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(article: Article) @@ -23,6 +21,4 @@ interface NewsDao { @Query("SELECT * FROM Article WHERE url=:url") suspend fun getArticle(url: String): Article? - - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/local/NewsDatabase.kt b/app/src/main/java/info/octera/droidstorybox/data/local/NewsDatabase.kt index eb210aa..620680e 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/local/NewsDatabase.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/local/NewsDatabase.kt @@ -5,10 +5,8 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import info.octera.droidstorybox.domain.model.Article -@Database(entities = [Article::class],version = 1,) +@Database(entities = [Article::class], version = 1) @TypeConverters(NewsTypeConverter::class) abstract class NewsDatabase : RoomDatabase() { - abstract val newsDao: NewsDao - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/local/NewsTypeConverter.kt b/app/src/main/java/info/octera/droidstorybox/data/local/NewsTypeConverter.kt index 13d8398..dd9be0c 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/local/NewsTypeConverter.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/local/NewsTypeConverter.kt @@ -4,20 +4,17 @@ import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter import info.octera.droidstorybox.domain.model.Source - @ProvidedTypeConverter class NewsTypeConverter { - - @TypeConverter - fun sourceToString(source: Source): String{ + fun sourceToString(source: Source): String { return "${source.id},${source.name}" } @TypeConverter - fun stringToSource(source: String): Source{ + fun stringToSource(source: String): Source { return source.split(',').let { sourceArray -> Source(id = sourceArray[0], name = sourceArray[1]) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/manager/LocalUserManagerImpl.kt b/app/src/main/java/info/octera/droidstorybox/data/manager/LocalUserManagerImpl.kt index 9b75c22..03abcab 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/manager/LocalUserManagerImpl.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/manager/LocalUserManagerImpl.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class LocalUserManagerImpl( - private val context: Context + private val context: Context, ) : LocalUserManager { override suspend fun saveAppEntry() { context.dataStore.edit { setting -> @@ -26,8 +26,9 @@ class LocalUserManagerImpl( } } } - private val Context.dataStore: DataStore by preferencesDataStore(name = Constants.USER_SETTINGS) - private object PreferencesKeys { - val APP_ENTRY = booleanPreferencesKey(name = Constants.APP_ENTRY) - } +private val Context.dataStore: DataStore by preferencesDataStore(name = Constants.USER_SETTINGS) + +private object PreferencesKeys { + val APP_ENTRY = booleanPreferencesKey(name = Constants.APP_ENTRY) +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/remote/NewsApi.kt b/app/src/main/java/info/octera/droidstorybox/data/remote/NewsApi.kt index 5f49bcf..7409731 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/remote/NewsApi.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/remote/NewsApi.kt @@ -6,12 +6,11 @@ import retrofit2.http.GET import retrofit2.http.Query interface NewsApi { - @GET("everything") suspend fun getNews( @Query("page") page: Int, @Query("sources") sources: String, - @Query("apiKey") apiKey: String = API_KEY + @Query("apiKey") apiKey: String = API_KEY, ): NewsResponse @GET("everything") @@ -19,6 +18,6 @@ interface NewsApi { @Query("q") searchQuery: String, @Query("page") page: Int, @Query("sources") sources: String, - @Query("apiKey") apiKey: String = API_KEY + @Query("apiKey") apiKey: String = API_KEY, ): NewsResponse -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/remote/NewsPagingSource.kt b/app/src/main/java/info/octera/droidstorybox/data/remote/NewsPagingSource.kt index ed147e8..cbf999e 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/remote/NewsPagingSource.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/remote/NewsPagingSource.kt @@ -6,7 +6,7 @@ import info.octera.droidstorybox.domain.model.Article class NewsPagingSource( private val newsApi: NewsApi, - private val sources: String + private val sources: String, ) : PagingSource() { private var totalNewsCount = 0 @@ -15,33 +15,27 @@ class NewsPagingSource( return try { val newsResponse = newsApi.getNews(sources = sources, page = page) totalNewsCount += newsResponse.articles.size - val articles = newsResponse.articles.distinctBy { - it.title - } + val articles = + newsResponse.articles.distinctBy { + it.title + } LoadResult.Page( data = articles, prevKey = if (page == 1) null else page - 1, - nextKey = if (totalNewsCount >= newsResponse.totalResults) null else page + 1 + nextKey = if (totalNewsCount >= newsResponse.totalResults) null else page + 1, ) } catch (e: Exception) { e.printStackTrace() LoadResult.Error( - throwable = e + throwable = e, ) } - - } override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } - - - - } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/remote/SearchNewsPagingSource.kt b/app/src/main/java/info/octera/droidstorybox/data/remote/SearchNewsPagingSource.kt index 767fdd3..f210481 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/remote/SearchNewsPagingSource.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/remote/SearchNewsPagingSource.kt @@ -7,14 +7,11 @@ import info.octera.droidstorybox.domain.model.Article class SearchNewsPagingSource( private val searchQuery: String, private val newsApi: NewsApi, - private val sources: String -): PagingSource() { - + private val sources: String, +) : PagingSource() { private var totalNewsCount = 0 - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) @@ -24,21 +21,22 @@ class SearchNewsPagingSource( override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 return try { - val newsResponse = newsApi.searchNews(searchQuery = searchQuery,sources = sources, page = page) + val newsResponse = newsApi.searchNews(searchQuery = searchQuery, sources = sources, page = page) totalNewsCount += newsResponse.articles.size - val articles = newsResponse.articles.distinctBy { - it.title - } + val articles = + newsResponse.articles.distinctBy { + it.title + } LoadResult.Page( data = articles, prevKey = if (page == 1) null else page - 1, - nextKey = if (totalNewsCount >= newsResponse.totalResults) null else page + 1 + nextKey = if (totalNewsCount >= newsResponse.totalResults) null else page + 1, ) } catch (e: Exception) { e.printStackTrace() LoadResult.Error( - throwable = e + throwable = e, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/data/remote/dto/NewsResponse.kt b/app/src/main/java/info/octera/droidstorybox/data/remote/dto/NewsResponse.kt index ae0721f..6dfea0f 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/remote/dto/NewsResponse.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/remote/dto/NewsResponse.kt @@ -5,5 +5,5 @@ import info.octera.droidstorybox.domain.model.Article data class NewsResponse( val articles: List
, val status: String, - val totalResults: Int -) \ No newline at end of file + val totalResults: Int, +) diff --git a/app/src/main/java/info/octera/droidstorybox/data/repository/NewsRepositoryImpl.kt b/app/src/main/java/info/octera/droidstorybox/data/repository/NewsRepositoryImpl.kt index 002e569..f339bc8 100644 --- a/app/src/main/java/info/octera/droidstorybox/data/repository/NewsRepositoryImpl.kt +++ b/app/src/main/java/info/octera/droidstorybox/data/repository/NewsRepositoryImpl.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.Flow class NewsRepositoryImpl( private val newsApi: NewsApi, - private val newsDao: NewsDao + private val newsDao: NewsDao, ) : NewsRepository { override fun getNews(sources: List): Flow> { return Pager( @@ -21,25 +21,27 @@ class NewsRepositoryImpl( pagingSourceFactory = { NewsPagingSource( newsApi = newsApi, - sources = sources.joinToString(",") + sources = sources.joinToString(","), ) - } + }, ).flow } - override fun searchNews(searchQuery: String, sources: List): Flow> { + override fun searchNews( + searchQuery: String, + sources: List, + ): Flow> { return Pager( config = PagingConfig(pageSize = 10), pagingSourceFactory = { SearchNewsPagingSource( searchQuery = searchQuery, newsApi = newsApi, - sources = sources.joinToString(",") + sources = sources.joinToString(","), ) - } + }, ).flow - } - + } override suspend fun upsertArticle(article: Article) { newsDao.upsert(article) @@ -56,4 +58,4 @@ class NewsRepositoryImpl( override suspend fun getArticle(url: String): Article? { return newsDao.getArticle(url = url) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/di/AppModule.kt b/app/src/main/java/info/octera/droidstorybox/di/AppModule.kt index c5001d2..35d4fe1 100644 --- a/app/src/main/java/info/octera/droidstorybox/di/AppModule.kt +++ b/app/src/main/java/info/octera/droidstorybox/di/AppModule.kt @@ -2,6 +2,10 @@ package info.octera.droidstorybox.di import android.app.Application import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import info.octera.droidstorybox.data.local.NewsDao import info.octera.droidstorybox.data.local.NewsDatabase import info.octera.droidstorybox.data.local.NewsTypeConverter @@ -22,35 +26,24 @@ import info.octera.droidstorybox.domain.usecases.news.SelectArticles import info.octera.droidstorybox.domain.usecases.news.UpsertArticle import info.octera.droidstorybox.util.Constants.BASE_URL import info.octera.droidstorybox.util.Constants.NEW_DATABASE -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Singleton - @Module @InstallIn(SingletonComponent::class) object AppModule { - @Provides @Singleton - fun provideLocalUserManager( - application: Application - ): LocalUserManager = LocalUserManagerImpl(application) - + fun provideLocalUserManager(application: Application): LocalUserManager = LocalUserManagerImpl(application) @Provides @Singleton - fun provideAppEntryUseCases( - localUserManger: LocalUserManager - ): AppEntryUseCases = AppEntryUseCases( - readAppEntry = ReadAppEntry(localUserManger), - saveAppEntry = SaveAppEntry(localUserManger) - ) - + fun provideAppEntryUseCases(localUserManger: LocalUserManager): AppEntryUseCases = + AppEntryUseCases( + readAppEntry = ReadAppEntry(localUserManger), + saveAppEntry = SaveAppEntry(localUserManger), + ) @Provides @Singleton @@ -64,15 +57,16 @@ object AppModule { @Provides @Singleton - fun provideNewsRepository(newsApi: NewsApi, newsDao: NewsDao): NewsRepository { + fun provideNewsRepository( + newsApi: NewsApi, + newsDao: NewsDao, + ): NewsRepository { return NewsRepositoryImpl(newsApi, newsDao) } @Provides @Singleton - fun provideNewsUseCases( - newsRepository: NewsRepository, - ): NewsUseCases { + fun provideNewsUseCases(newsRepository: NewsRepository): NewsUseCases { return NewsUseCases( getNews = GetNews(newsRepository), searchNews = SearchNews(newsRepository), @@ -83,29 +77,21 @@ object AppModule { ) } - @Provides @Singleton - fun provideNewsDatabase( - application: Application - ): NewsDatabase { + fun provideNewsDatabase(application: Application): NewsDatabase { return Room.databaseBuilder( context = application, klass = NewsDatabase::class.java, - name = NEW_DATABASE + name = NEW_DATABASE, ).addTypeConverter(NewsTypeConverter()) .fallbackToDestructiveMigration() .build() - } @Provides @Singleton - fun provideNewsDao( - newsDatabase: NewsDatabase - ): NewsDao { + fun provideNewsDao(newsDatabase: NewsDatabase): NewsDao { return newsDatabase.newsDao } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/manager/LocalUserManager.kt b/app/src/main/java/info/octera/droidstorybox/domain/manager/LocalUserManager.kt index f7bab08..3d5df27 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/manager/LocalUserManager.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/manager/LocalUserManager.kt @@ -3,8 +3,7 @@ package info.octera.droidstorybox.domain.manager import kotlinx.coroutines.flow.Flow interface LocalUserManager { - suspend fun saveAppEntry() fun readAppEntry(): Flow -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/model/Article.kt b/app/src/main/java/info/octera/droidstorybox/domain/model/Article.kt index c4b57e2..732bc78 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/model/Article.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/model/Article.kt @@ -15,5 +15,5 @@ data class Article( val source: Source, val title: String, @PrimaryKey val url: String, - val urlToImage: String -): Parcelable \ No newline at end of file + val urlToImage: String, +) : Parcelable diff --git a/app/src/main/java/info/octera/droidstorybox/domain/model/Source.kt b/app/src/main/java/info/octera/droidstorybox/domain/model/Source.kt index 32f24a2..e5b1c25 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/model/Source.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/model/Source.kt @@ -3,9 +3,8 @@ package info.octera.droidstorybox.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize - @Parcelize data class Source( val id: String, - val name: String -): Parcelable \ No newline at end of file + val name: String, +) : Parcelable diff --git a/app/src/main/java/info/octera/droidstorybox/domain/repository/NewsRepository.kt b/app/src/main/java/info/octera/droidstorybox/domain/repository/NewsRepository.kt index aadce3c..741a8ba 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/repository/NewsRepository.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/repository/NewsRepository.kt @@ -5,10 +5,12 @@ import info.octera.droidstorybox.domain.model.Article import kotlinx.coroutines.flow.Flow interface NewsRepository { - fun getNews(sources : List): Flow> + fun getNews(sources: List): Flow> - - fun searchNews(searchQuery : String, sources : List): Flow> + fun searchNews( + searchQuery: String, + sources: List, + ): Flow> suspend fun upsertArticle(article: Article) diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/AppEntryUseCases.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/AppEntryUseCases.kt index e5bc3d3..25b320b 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/AppEntryUseCases.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/AppEntryUseCases.kt @@ -2,5 +2,5 @@ package info.octera.droidstorybox.domain.usecases.app_entry data class AppEntryUseCases( val readAppEntry: ReadAppEntry, - val saveAppEntry: SaveAppEntry -) \ No newline at end of file + val saveAppEntry: SaveAppEntry, +) diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/ReadAppEntry.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/ReadAppEntry.kt index 933af3a..3848bc1 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/ReadAppEntry.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/ReadAppEntry.kt @@ -4,10 +4,9 @@ import info.octera.droidstorybox.domain.manager.LocalUserManager import kotlinx.coroutines.flow.Flow class ReadAppEntry( - private val localUserManager : LocalUserManager + private val localUserManager: LocalUserManager, ) { - - operator fun invoke() : Flow { - return localUserManager.readAppEntry() + operator fun invoke(): Flow { + return localUserManager.readAppEntry() } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/SaveAppEntry.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/SaveAppEntry.kt index 3039a7b..c5c3de0 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/SaveAppEntry.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/app_entry/SaveAppEntry.kt @@ -3,10 +3,9 @@ package info.octera.droidstorybox.domain.usecases.app_entry import info.octera.droidstorybox.domain.manager.LocalUserManager class SaveAppEntry( - private val localUserManager : LocalUserManager + private val localUserManager: LocalUserManager, ) { - - suspend operator fun invoke(){ + suspend operator fun invoke() { localUserManager.saveAppEntry() } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/DeleteArticle.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/DeleteArticle.kt index ed1f352..8d2fe5a 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/DeleteArticle.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/DeleteArticle.kt @@ -3,12 +3,10 @@ package info.octera.droidstorybox.domain.usecases.news import info.octera.droidstorybox.domain.model.Article import info.octera.droidstorybox.domain.repository.NewsRepository -class DeleteArticle ( - private val newsRepository: NewsRepository +class DeleteArticle( + private val newsRepository: NewsRepository, ) { - - suspend operator fun invoke(article: Article){ + suspend operator fun invoke(article: Article) { newsRepository.deleteArticle(article = article) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/GetNews.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/GetNews.kt index 21e93de..0761f15 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/GetNews.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/GetNews.kt @@ -6,11 +6,9 @@ import info.octera.droidstorybox.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow class GetNews( - private val newsRepository: NewsRepository + private val newsRepository: NewsRepository, ) { - operator fun invoke(sources: List): Flow> { return newsRepository.getNews(sources = sources) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/NewsUseCases.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/NewsUseCases.kt index 92fe496..f0627fc 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/NewsUseCases.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/NewsUseCases.kt @@ -1,7 +1,5 @@ package info.octera.droidstorybox.domain.usecases.news - - data class NewsUseCases( val getNews: GetNews, val searchNews: SearchNews, @@ -9,4 +7,4 @@ data class NewsUseCases( val deleteArticle: DeleteArticle, val selectArticles: SelectArticles, val selectArticle: SelectArticle, -) \ No newline at end of file +) diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SearchNews.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SearchNews.kt index 109f253..be40183 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SearchNews.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SearchNews.kt @@ -6,11 +6,12 @@ import info.octera.droidstorybox.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow class SearchNews( - private val newsRepository: NewsRepository + private val newsRepository: NewsRepository, ) { - - operator fun invoke(searchQuery : String ,sources: List): Flow> { + operator fun invoke( + searchQuery: String, + sources: List, + ): Flow> { return newsRepository.searchNews(searchQuery = searchQuery, sources = sources) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticle.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticle.kt index c35831d..6bd6c8b 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticle.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticle.kt @@ -4,9 +4,9 @@ import info.octera.droidstorybox.domain.model.Article import info.octera.droidstorybox.domain.repository.NewsRepository class SelectArticle( - private val newsRepository: NewsRepository + private val newsRepository: NewsRepository, ) { - suspend operator fun invoke(url: String): Article? { - return newsRepository.getArticle(url) + suspend operator fun invoke(url: String): Article? { + return newsRepository.getArticle(url) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticles.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticles.kt index ef2c5ed..0cb9229 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticles.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/SelectArticles.kt @@ -5,11 +5,9 @@ import info.octera.droidstorybox.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow class SelectArticles( - private val newsRepository: NewsRepository + private val newsRepository: NewsRepository, ) { - operator fun invoke(): Flow> { return newsRepository.getArticles() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/UpsertArticle.kt b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/UpsertArticle.kt index 4a71adf..b1c00f9 100644 --- a/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/UpsertArticle.kt +++ b/app/src/main/java/info/octera/droidstorybox/domain/usecases/news/UpsertArticle.kt @@ -4,11 +4,9 @@ import info.octera.droidstorybox.domain.model.Article import info.octera.droidstorybox.domain.repository.NewsRepository class UpsertArticle( - private val newsRepository: NewsRepository + private val newsRepository: NewsRepository, ) { - - suspend operator fun invoke(article: Article){ + suspend operator fun invoke(article: Article) { newsRepository.upsertArticle(article = article) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/Dimens.kt b/app/src/main/java/info/octera/droidstorybox/presentation/Dimens.kt index 6a9e11e..03a2c63 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/Dimens.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/Dimens.kt @@ -3,8 +3,6 @@ package info.octera.droidstorybox.presentation import androidx.compose.ui.unit.dp object Dimens { - - val ExtraSmallPadding = 3.dp val ExtraSmallPadding2 = 6.dp val MediumPadding1 = 24.dp @@ -19,4 +17,4 @@ object Dimens { val ArticleCardSize = 96.dp val ArticleImageHeight = 248.dp -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkScreen.kt index e2ff7c5..de83228 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkScreen.kt @@ -21,29 +21,31 @@ import info.octera.droidstorybox.presentation.common.ArticlesList fun BookmarkScreen( modifier: Modifier = Modifier, state: BookmarkState, - navigateToDetails: (Article) -> Unit + navigateToDetails: (Article) -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding() - .padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1) + modifier = + Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1), ) { - Text( text = "Bookmark", style = MaterialTheme.typography.displayMedium.copy(fontWeight = FontWeight.Bold), - color = colorResource( - id = R.color.text_title - ) + color = + colorResource( + id = R.color.text_title, + ), ) Spacer(modifier = Modifier.height(MediumPadding1)) ArticlesList( articles = state.articles, - onClick = {navigateToDetails(it) - } + onClick = { + navigateToDetails(it) + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkState.kt b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkState.kt index 0a757bb..aa953e3 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkState.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkState.kt @@ -4,5 +4,4 @@ import info.octera.droidstorybox.domain.model.Article data class BookmarkState( val articles: List
= emptyList(), - -) \ No newline at end of file +) diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkViewModel.kt b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkViewModel.kt index d714f65..d5d2acb 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/bookmark/BookmarkViewModel.kt @@ -4,28 +4,28 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import dagger.hilt.android.lifecycle.HiltViewModel +import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject @HiltViewModel -class BookmarkViewModel @Inject constructor( - private val newsUseCases: NewsUseCases -) : ViewModel() -{ +class BookmarkViewModel + @Inject + constructor( + private val newsUseCases: NewsUseCases, + ) : ViewModel() { + private val _state = mutableStateOf(BookmarkState()) + val state: State = _state - private val _state = mutableStateOf(BookmarkState()) - val state: State = _state - - init { - getArticles() - } + init { + getArticles() + } - private fun getArticles() { - newsUseCases.selectArticles().onEach { - _state.value = _state.value.copy(articles = it.reversed()) - }.launchIn(viewModelScope) + private fun getArticles() { + newsUseCases.selectArticles().onEach { + _state.value = _state.value.copy(articles = it.reversed()) + }.launchIn(viewModelScope) + } } -} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticleCard.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticleCard.kt index 4f30ab2..610faad 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticleCard.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticleCard.kt @@ -38,55 +38,55 @@ import info.octera.droidstorybox.ui.theme.NewsAppTheme fun ArticleCard( modifier: Modifier = Modifier, article: Article, - onClick: (() -> Unit)? = null + onClick: (() -> Unit)? = null, ) { - val context = LocalContext.current Row( modifier = modifier.clickable { onClick?.invoke() }, - - ) { + ) { AsyncImage( - modifier = Modifier - .size(ArticleCardSize) - .clip(MaterialTheme.shapes.medium), + modifier = + Modifier + .size(ArticleCardSize) + .clip(MaterialTheme.shapes.medium), model = ImageRequest.Builder(context).data(article.urlToImage).build(), contentDescription = null, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, ) Column( verticalArrangement = Arrangement.SpaceAround, - modifier = Modifier - .padding(horizontal = ExtraSmallPadding) - .height(ArticleCardSize) + modifier = + Modifier + .padding(horizontal = ExtraSmallPadding) + .height(ArticleCardSize), ) { Text( text = article.title, style = MaterialTheme.typography.bodyMedium.copy(), color = colorResource(id = R.color.text_title), maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = article.source.name, style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold), - color = colorResource(id = R.color.body) + color = colorResource(id = R.color.body), ) Spacer(modifier = Modifier.width(ExtraSmallPadding2)) Icon( painter = painterResource(id = R.drawable.baseline_access_time_24), contentDescription = null, modifier = Modifier.size(SmallIconSize), - tint = colorResource(id = R.color.body) + tint = colorResource(id = R.color.body), ) Spacer(modifier = Modifier.width(ExtraSmallPadding)) Text( text = article.publishedAt, style = MaterialTheme.typography.labelSmall, - color = colorResource(id = R.color.body) + color = colorResource(id = R.color.body), ) } } @@ -98,16 +98,17 @@ fun ArticleCard( fun ArticleCardPreview() { NewsAppTheme(dynamicColor = false) { ArticleCard( - article = Article( - author = "", - content = "", - description = "", - publishedAt = "2 hours", - source = Source(id = "", name = "BBC"), - title = "Her train broke down. Her phone died. And then she met her Saver in a", - url = "", - urlToImage = "https://ichef.bbci.co.uk/live-experience/cps/624/cpsprodpb/11787/production/_124395517_bbcbreakingnewsgraphic.jpg" - ) + article = + Article( + author = "", + content = "", + description = "", + publishedAt = "2 hours", + source = Source(id = "", name = "BBC"), + title = "Her train broke down. Her phone died. And then she met her Saver in a", + url = "", + urlToImage = "https://ichef.bbci.co.uk/live-experience/cps/624/cpsprodpb/11787/production/_124395517_bbcbreakingnewsgraphic.jpg", + ), ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticlesList.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticlesList.kt index 4417c4f..a4e4e45 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticlesList.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/ArticlesList.kt @@ -15,20 +15,20 @@ import info.octera.droidstorybox.presentation.Dimens.ExtraSmallPadding2 import info.octera.droidstorybox.presentation.Dimens.MediumPadding1 import info.octera.droidstorybox.presentation.Dimens.MediumPadding2 - @Composable fun ArticlesList( modifier: Modifier = Modifier, articles: List
, - onClick: (Article) -> Unit + onClick: (Article) -> Unit, ) { - if (articles.isEmpty()){ - EmptyScreen() - } + if (articles.isEmpty()) + { + EmptyScreen() + } LazyColumn( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(MediumPadding1), - contentPadding = PaddingValues(all = ExtraSmallPadding2) + contentPadding = PaddingValues(all = ExtraSmallPadding2), ) { items( count = articles.size, @@ -38,26 +38,21 @@ fun ArticlesList( } } } - } - - @Composable fun ArticlesList( modifier: Modifier = Modifier, articles: LazyPagingItems
, - onClick: (Article) -> Unit + onClick: (Article) -> Unit, ) { - val handlePagingResult = handlePagingResult(articles = articles) - if (handlePagingResult) { LazyColumn( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(MediumPadding1), - contentPadding = PaddingValues(all = ExtraSmallPadding2) + contentPadding = PaddingValues(all = ExtraSmallPadding2), ) { items( count = articles.itemCount, @@ -73,16 +68,16 @@ fun ArticlesList( @Composable fun handlePagingResult( modifier: Modifier = Modifier, - articles: LazyPagingItems
+ articles: LazyPagingItems
, ): Boolean { - val loadState = articles.loadState - val error = when { - loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error - loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error - loadState.append is LoadState.Error -> loadState.append as LoadState.Error - else -> null - } + val error = + when { + loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error + loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error + loadState.append is LoadState.Error -> loadState.append as LoadState.Error + else -> null + } return when { loadState.refresh is LoadState.Loading -> { @@ -94,8 +89,6 @@ fun handlePagingResult( true } } - - } @Composable @@ -103,38 +96,8 @@ private fun ShimmerEffect(modifier: Modifier = Modifier) { Column(verticalArrangement = Arrangement.spacedBy(MediumPadding2)) { repeat(10) { ArticleCardShimmerEffect( - modifier = Modifier.padding(horizontal = MediumPadding2) + modifier = Modifier.padding(horizontal = MediumPadding2), ) } } } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/EmptyScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/EmptyScreen.kt index 8b67c2b..d5735ff 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/EmptyScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/EmptyScreen.kt @@ -34,7 +34,6 @@ import java.net.SocketTimeoutException @Composable fun EmptyScreen(error: LoadState.Error? = null) { - var message by remember { mutableStateOf(parseErrorMessage(error = error)) } @@ -54,7 +53,8 @@ fun EmptyScreen(error: LoadState.Error? = null) { val alphaAnimation by animateFloatAsState( targetValue = if (startAnimation) 0.3f else 0f, - animationSpec = tween(durationMillis = 1000), label = "" + animationSpec = tween(durationMillis = 1000), + label = "", ) LaunchedEffect(key1 = true) { @@ -62,28 +62,33 @@ fun EmptyScreen(error: LoadState.Error? = null) { } EmptyContent(alphaAnim = alphaAnimation, message = message, iconId = icon) - } @Composable -fun EmptyContent(alphaAnim: Float, message: String, iconId: Int) { +fun EmptyContent( + alphaAnim: Float, + message: String, + iconId: Int, +) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Icon( painter = painterResource(id = iconId), contentDescription = null, tint = if (isSystemInDarkTheme()) LightGray else DarkGray, - modifier = Modifier - .size(120.dp) - .alpha(alphaAnim) + modifier = + Modifier + .size(120.dp) + .alpha(alphaAnim), ) Text( - modifier = Modifier - .padding(10.dp) - .alpha(alphaAnim), + modifier = + Modifier + .padding(10.dp) + .alpha(alphaAnim), text = message, style = MaterialTheme.typography.bodyMedium, color = if (isSystemInDarkTheme()) LightGray else DarkGray, @@ -91,7 +96,6 @@ fun EmptyContent(alphaAnim: Float, message: String, iconId: Int) { } } - fun parseErrorMessage(error: LoadState.Error?): String { return when (error?.error) { is SocketTimeoutException -> { @@ -115,6 +119,6 @@ fun EmptyScreenPreview() { EmptyContent( alphaAnim = 0.3f, message = "Internet Unavailable.", - R.drawable.baseline_signal_cellular_connected_no_internet_0_bar_24 + R.drawable.baseline_signal_cellular_connected_no_internet_0_bar_24, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/NewsButton.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/NewsButton.kt index 0494f60..e84c71e 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/NewsButton.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/NewsButton.kt @@ -13,36 +13,46 @@ import androidx.compose.ui.unit.dp import info.octera.droidstorybox.ui.theme.Black @Composable -fun NewsButton(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) { +fun NewsButton( + modifier: Modifier = Modifier, + text: String, + onClick: () -> Unit, +) { Button( - onClick = onClick, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = White - ), shape = RoundedCornerShape(6.dp) + onClick = onClick, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = White, + ), + shape = RoundedCornerShape(6.dp), ) { Text( text = text, - style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), ) } - } @Composable -fun NewsTextButton(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) { +fun NewsTextButton( + modifier: Modifier = Modifier, + text: String, + onClick: () -> Unit, +) { Button( - onClick = onClick, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.background, - contentColor = Black - ), shape = RoundedCornerShape(6.dp) + onClick = onClick, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.background, + contentColor = Black, + ), + shape = RoundedCornerShape(6.dp), ) { Text( text = text, style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), - color = Black + color = Black, ) } } - - - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/SearchBar.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/SearchBar.kt index 0b48212..14a469c 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/SearchBar.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/SearchBar.kt @@ -1,6 +1,5 @@ package info.octera.droidstorybox.presentation.common - import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource @@ -40,12 +39,12 @@ fun SearchBar( readOnly: Boolean, onClick: (() -> Unit)? = null, onValueChange: (String) -> Unit, - onSearch: () -> Unit + onSearch: () -> Unit, ) { - - val interactionSource = remember { - MutableInteractionSource() - } + val interactionSource = + remember { + MutableInteractionSource() + } val isClicked = interactionSource.collectIsPressedAsState().value LaunchedEffect(key1 = isClicked) { if (isClicked) { @@ -55,9 +54,10 @@ fun SearchBar( Box(modifier = modifier) { TextField( - modifier = Modifier - .fillMaxWidth() - .searchBar(), + modifier = + Modifier + .fillMaxWidth() + .searchBar(), value = text, onValueChange = onValueChange, readOnly = readOnly, @@ -66,50 +66,53 @@ fun SearchBar( painter = painterResource(id = R.drawable.baseline_search_24), contentDescription = null, modifier = Modifier.size(IconSize), - tint = colorResource(id = R.color.body) + tint = colorResource(id = R.color.body), ) }, placeholder = { Text( text = "Search", style = MaterialTheme.typography.bodySmall, - color = colorResource(id = R.color.placeholder) + color = colorResource(id = R.color.placeholder), ) }, shape = MaterialTheme.shapes.medium, - colors = TextFieldDefaults.textFieldColors( - containerColor = colorResource(id = R.color.input_background), - focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black, - cursorColor = if (isSystemInDarkTheme()) Color.White else Color.Black, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ), + colors = + TextFieldDefaults.textFieldColors( + containerColor = colorResource(id = R.color.input_background), + focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black, + cursorColor = if (isSystemInDarkTheme()) Color.White else Color.Black, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - onSearch() - } - ), + keyboardActions = + KeyboardActions( + onSearch = { + onSearch() + }, + ), textStyle = MaterialTheme.typography.bodySmall, - interactionSource = interactionSource + interactionSource = interactionSource, ) } } -fun Modifier.searchBar(): Modifier = composed { - if (!isSystemInDarkTheme()) { - border( - width = 1.dp, - color = Color.Black, - shape = MaterialTheme.shapes.medium - ) - } else { - this +fun Modifier.searchBar(): Modifier = + composed { + if (!isSystemInDarkTheme()) { + border( + width = 1.dp, + color = Color.Black, + shape = MaterialTheme.shapes.medium, + ) + } else { + this + } } -} @Preview(showBackground = true) @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) @@ -117,15 +120,6 @@ fun Modifier.searchBar(): Modifier = composed { fun SearchBarPreview() { NewsAppTheme { SearchBar(text = "", onValueChange = {}, readOnly = false) { - } } } - - - - - - - - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/common/ShimmerEffects.kt b/app/src/main/java/info/octera/droidstorybox/presentation/common/ShimmerEffects.kt index 17e848a..26426eb 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/common/ShimmerEffects.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/common/ShimmerEffects.kt @@ -29,54 +29,62 @@ import info.octera.droidstorybox.presentation.Dimens import info.octera.droidstorybox.presentation.Dimens.MediumPadding1 import info.octera.droidstorybox.ui.theme.NewsAppTheme - @SuppressLint("ModifierFactoryUnreferencedReceiver") -fun Modifier.shimmerEffect() = composed { - val transition = rememberInfiniteTransition(label = "") - val alpha = transition.animateFloat( - initialValue = 0.2f, targetValue = 0.9f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000), - repeatMode = RepeatMode.Reverse - ), label = "" - ).value - background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha)) -} +fun Modifier.shimmerEffect() = + composed { + val transition = rememberInfiniteTransition(label = "") + val alpha = + transition.animateFloat( + initialValue = 0.2f, + targetValue = 0.9f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000), + repeatMode = RepeatMode.Reverse, + ), + label = "", + ).value + background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha)) + } @Composable fun ArticleCardShimmerEffect(modifier: Modifier = Modifier) { Row( - modifier = modifier + modifier = modifier, ) { Box( - modifier = Modifier - .size(Dimens.ArticleCardSize) - .clip(MaterialTheme.shapes.medium) - .shimmerEffect() + modifier = + Modifier + .size(Dimens.ArticleCardSize) + .clip(MaterialTheme.shapes.medium) + .shimmerEffect(), ) Column( verticalArrangement = Arrangement.SpaceAround, - modifier = Modifier - .padding(horizontal = Dimens.ExtraSmallPadding) - .height(Dimens.ArticleCardSize) + modifier = + Modifier + .padding(horizontal = Dimens.ExtraSmallPadding) + .height(Dimens.ArticleCardSize), ) { Box( - modifier = Modifier - .fillMaxWidth() - .height(30.dp) - .padding(horizontal = MediumPadding1) - .shimmerEffect() + modifier = + Modifier + .fillMaxWidth() + .height(30.dp) + .padding(horizontal = MediumPadding1) + .shimmerEffect(), ) Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Box( - modifier = Modifier - .fillMaxWidth(0.5f) - .padding(horizontal = MediumPadding1) - .height(15.dp) - .shimmerEffect() + modifier = + Modifier + .fillMaxWidth(0.5f) + .padding(horizontal = MediumPadding1) + .height(15.dp) + .shimmerEffect(), ) - } } } @@ -88,4 +96,4 @@ private fun ArticlePreview() { NewsAppTheme { ArticleCardShimmerEffect() } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailViewModel.kt b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailViewModel.kt index 2dae7a7..2a586ce 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailViewModel.kt @@ -5,47 +5,47 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import info.octera.droidstorybox.domain.model.Article import info.octera.droidstorybox.domain.usecases.news.NewsUseCases -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject - @HiltViewModel -class DetailViewModel @Inject constructor( - private val newsUseCases: NewsUseCases -) : ViewModel() { - var sideEffect by mutableStateOf(null) - private set - - fun onEvent(event: DetailsEvent) { - when (event) { - is DetailsEvent.UpsertDeleteArticle -> { - viewModelScope.launch { - val article = newsUseCases.selectArticle(event.article.url) - if (article == null) { - upsertArticle(event.article) - } else { - deleteArticle(event.article) +class DetailViewModel + @Inject + constructor( + private val newsUseCases: NewsUseCases, + ) : ViewModel() { + var sideEffect by mutableStateOf(null) + private set + + fun onEvent(event: DetailsEvent) { + when (event) { + is DetailsEvent.UpsertDeleteArticle -> { + viewModelScope.launch { + val article = newsUseCases.selectArticle(event.article.url) + if (article == null) { + upsertArticle(event.article) + } else { + deleteArticle(event.article) + } } } - } - is DetailsEvent.RemoveSideEffect -> { - sideEffect = null + is DetailsEvent.RemoveSideEffect -> { + sideEffect = null + } } } - } - private suspend fun deleteArticle(article: Article) { - newsUseCases.deleteArticle(article = article) - sideEffect = "Article is removed form saved" - } + private suspend fun deleteArticle(article: Article) { + newsUseCases.deleteArticle(article = article) + sideEffect = "Article is removed form saved" + } - private suspend fun upsertArticle(article: Article) { - newsUseCases.upsertArticle(article = article) - sideEffect = "Article is been saved" + private suspend fun upsertArticle(article: Article) { + newsUseCases.upsertArticle(article = article) + sideEffect = "Article is been saved" + } } - -} \ No newline at end of file diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsEvent.kt b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsEvent.kt index 79bbce8..e93ab15 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsEvent.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsEvent.kt @@ -3,9 +3,7 @@ package info.octera.droidstorybox.presentation.details import info.octera.droidstorybox.domain.model.Article sealed class DetailsEvent { - data class UpsertDeleteArticle(val article: Article) : DetailsEvent() object RemoveSideEffect : DetailsEvent() - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsScreen.kt index 1f57420..2dcc64a 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/details/DetailsScreen.kt @@ -33,14 +33,18 @@ import info.octera.droidstorybox.ui.theme.NewsAppTheme @Composable fun DetailsScreen( - modifier: Modifier = Modifier, article: Article, + modifier: Modifier = Modifier, + article: Article, event: (DetailsEvent) -> Unit, - navigateUp: () -> Unit + navigateUp: () -> Unit, ) { val context = LocalContext.current - Column(modifier = Modifier - .fillMaxSize() - .statusBarsPadding()) { + Column( + modifier = + Modifier + .fillMaxSize() + .statusBarsPadding(), + ) { DetailsTopBar( onBrowsingClick = { Intent(Intent.ACTION_VIEW).also { @@ -62,42 +66,47 @@ fun DetailsScreen( onBookMarkClick = { event(DetailsEvent.UpsertDeleteArticle(article)) }, - onBackClick = navigateUp + onBackClick = navigateUp, ) LazyColumn( modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues( - start = MediumPadding1, - end = MediumPadding1, - top = MediumPadding1 - ) + contentPadding = + PaddingValues( + start = MediumPadding1, + end = MediumPadding1, + top = MediumPadding1, + ), ) { item { AsyncImage( - model = ImageRequest.Builder(context = context).data(article.urlToImage) - .build(), + model = + ImageRequest.Builder(context = context).data(article.urlToImage) + .build(), contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(ArticleImageHeight) - .clip(MaterialTheme.shapes.medium), - contentScale = ContentScale.Crop + modifier = + Modifier + .fillMaxWidth() + .height(ArticleImageHeight) + .clip(MaterialTheme.shapes.medium), + contentScale = ContentScale.Crop, ) Spacer(modifier = Modifier.height(MediumPadding1)) Text( text = article.title, style = MaterialTheme.typography.displaySmall, - color = colorResource( - id = R.color.text_title - ) + color = + colorResource( + id = R.color.text_title, + ), ) Text( text = article.content, style = MaterialTheme.typography.bodyMedium, - color = colorResource( - id = R.color.body - ) + color = + colorResource( + id = R.color.body, + ), ) Spacer(modifier = Modifier.padding(top = 20.dp)) } @@ -110,21 +119,23 @@ fun DetailsScreen( fun DetailsScreenPreview() { NewsAppTheme(dynamicColor = false) { DetailsScreen( - article = Article( - author = "", - title = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", - description = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", - content = "We use cookies and data to Deliver and maintain Google services Track outages and protect against spam, fraud, and abuse Measure audience engagement and site statistics ", - publishedAt = "2023-06-16T22:24:33Z", - source = Source( - id = "", name = "bbc" + article = + Article( + author = "", + title = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", + description = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", + content = "We use cookies and data to Deliver and maintain Google services Track outages and protect against spam, fraud, and abuse Measure audience engagement and site statistics ", + publishedAt = "2023-06-16T22:24:33Z", + source = + Source( + id = "", + name = "bbc", + ), + url = "https://consent.google.com/ml?continue=https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vY3J5cHRvc2F1cnVzLnRlY2gvY29pbmJhc2Utc2F5cy1hcHBsZS1ibG9ja2VkLWl0cy1sYXN0LWFwcC1yZWxlYXNlLW9uLW5mdHMtaW4td2FsbGV0LXJldXRlcnMtY29tL9IBAA?oc%3D5&gl=FR&hl=en-US&cm=2&pc=n&src=1", + urlToImage = "https://media.wired.com/photos/6495d5e893ba5cd8bbdc95af/191:100/w_1280,c_limit/The-EU-Rules-Phone-Batteries-Must-Be-Replaceable-Gear-2BE6PRN.jpg", ), - url = "https://consent.google.com/ml?continue=https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vY3J5cHRvc2F1cnVzLnRlY2gvY29pbmJhc2Utc2F5cy1hcHBsZS1ibG9ja2VkLWl0cy1sYXN0LWFwcC1yZWxlYXNlLW9uLW5mdHMtaW4td2FsbGV0LXJldXRlcnMtY29tL9IBAA?oc%3D5&gl=FR&hl=en-US&cm=2&pc=n&src=1", - urlToImage = "https://media.wired.com/photos/6495d5e893ba5cd8bbdc95af/191:100/w_1280,c_limit/The-EU-Rules-Phone-Batteries-Must-Be-Replaceable-Gear-2BE6PRN.jpg" - ), - event = {} + event = {}, ) { - } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/details/components/DetailsTopBar.kt b/app/src/main/java/info/octera/droidstorybox/presentation/details/components/DetailsTopBar.kt index 6106960..45d2787 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/details/components/DetailsTopBar.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/details/components/DetailsTopBar.kt @@ -32,11 +32,12 @@ fun DetailsTopBar( ) { TopAppBar( modifier = Modifier.fillMaxWidth(), - colors = TopAppBarDefaults.mediumTopAppBarColors( - containerColor = Color.Transparent, - actionIconContentColor = colorResource(id = R.color.body), - navigationIconContentColor = colorResource(id = R.color.body), - ), + colors = + TopAppBarDefaults.mediumTopAppBarColors( + containerColor = Color.Transparent, + actionIconContentColor = colorResource(id = R.color.body), + navigationIconContentColor = colorResource(id = R.color.body), + ), title = {}, navigationIcon = { IconButton(onClick = onBackClick) { @@ -47,23 +48,22 @@ fun DetailsTopBar( } }, actions = { - IconButton(onClick = onBookMarkClick) { Icon( painter = painterResource(id = R.drawable.baseline_bookmark_24), - contentDescription = null + contentDescription = null, ) } IconButton(onClick = onShareClick) { Icon( imageVector = Icons.Default.Share, - contentDescription = null + contentDescription = null, ) } IconButton(onClick = onBrowsingClick) { Icon( painter = painterResource(id = R.drawable.baseline_web_24), - contentDescription = null + contentDescription = null, ) } }, @@ -75,14 +75,17 @@ fun DetailsTopBar( @Composable fun DetailsTopBarPreview() { NewsAppTheme(dynamicColor = false) { - Box(modifier = Modifier - .background(MaterialTheme.colorScheme.background)) { + Box( + modifier = + Modifier + .background(MaterialTheme.colorScheme.background), + ) { DetailsTopBar( onShareClick = { /*TODO*/ }, onBookMarkClick = { /*TODO*/ }, - onBrowsingClick = {}) { - + onBrowsingClick = {}, + ) { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeScreen.kt index f57e993..2b099ec 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeScreen.kt @@ -1,6 +1,5 @@ package info.octera.droidstorybox.presentation.home - import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee @@ -32,15 +31,13 @@ import info.octera.droidstorybox.presentation.Dimens.MediumPadding2 import info.octera.droidstorybox.presentation.common.ArticlesList import info.octera.droidstorybox.presentation.common.SearchBar - @OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( articles: LazyPagingItems
, navigateToSearch: () -> Unit, - navigateToDetails: (Article) -> Unit + navigateToDetails: (Article) -> Unit, ) { - val titles by remember { derivedStateOf { if (articles.itemCount > 10) { @@ -54,43 +51,49 @@ fun HomeScreen( } Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 10.dp) - .statusBarsPadding() + modifier = + Modifier + .fillMaxSize() + .padding(top = 10.dp) + .statusBarsPadding(), ) { Image( painter = painterResource(id = R.drawable.banner), contentDescription = null, - modifier = Modifier - .size(width = 900.dp, height = 30.dp) - .align(Alignment.CenterHorizontally) - , contentScale = ContentScale.Crop + modifier = + Modifier + .size(width = 900.dp, height = 30.dp) + .align(Alignment.CenterHorizontally), + contentScale = ContentScale.Crop, ) Spacer(modifier = Modifier.padding(10.dp)) SearchBar( - modifier = Modifier - .padding(horizontal = MediumPadding2) - .fillMaxWidth(), + modifier = + Modifier + .padding(horizontal = MediumPadding2) + .fillMaxWidth(), text = "", readOnly = true, onValueChange = {}, onSearch = {}, onClick = { navigateToSearch() - } + }, ) Spacer(modifier = Modifier.padding(bottom = 20.dp)) Text( - text = titles, modifier = Modifier - .fillMaxWidth() - .padding(start = MediumPadding1) - .basicMarquee(), fontSize = 14.sp, - color = colorResource(id = R.color.placeholder) + text = titles, + modifier = + Modifier + .fillMaxWidth() + .padding(start = MediumPadding1) + .basicMarquee(), + fontSize = 14.sp, + color = colorResource(id = R.color.placeholder), ) Spacer(modifier = Modifier.height(MediumPadding1)) @@ -100,7 +103,7 @@ fun HomeScreen( articles = articles, onClick = { navigateToDetails(it) - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeViewModel.kt b/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeViewModel.kt index ec9390c..0db4a2e 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/home/HomeViewModel.kt @@ -3,16 +3,18 @@ package info.octera.droidstorybox.presentation.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn -import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import dagger.hilt.android.lifecycle.HiltViewModel +import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import javax.inject.Inject @HiltViewModel -class HomeViewModel @Inject constructor( - private val newsUseCases: NewsUseCases -): ViewModel() { - - val news = newsUseCases.getNews( - sources = listOf("bbc-news", "abc-news", "al-jazeera-english") - ).cachedIn(viewModelScope) -} \ No newline at end of file +class HomeViewModel + @Inject + constructor( + private val newsUseCases: NewsUseCases, + ) : ViewModel() { + val news = + newsUseCases.getNews( + sources = listOf("bbc-news", "abc-news", "al-jazeera-english"), + ).cachedIn(viewModelScope) + } diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/NavGraph.kt b/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/NavGraph.kt index e05e4db..1648314 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/NavGraph.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/NavGraph.kt @@ -11,15 +11,13 @@ import info.octera.droidstorybox.presentation.onboarding.OnBoardingViewModel import info.octera.droidstorybox.presentation.onboarding.components.OnBoardingScreen @Composable -fun NavGraph( - startDestination: String -) { +fun NavGraph(startDestination: String) { val navController = rememberNavController() NavHost(navController = navController, startDestination = startDestination) { navigation( route = Route.AppStartNavigation.route, - startDestination = Route.OnBoardingScreen.route + startDestination = Route.OnBoardingScreen.route, ) { composable(route = Route.OnBoardingScreen.route) { val viewModel: OnBoardingViewModel = hiltViewModel() @@ -29,32 +27,11 @@ fun NavGraph( navigation( route = Route.NewsNavigation.route, - startDestination = Route.NewsNavigatorScreen.route + startDestination = Route.NewsNavigatorScreen.route, ) { composable(route = Route.NewsNavigatorScreen.route) { - NewsNavigator() - + NewsNavigator() } } - - } - - } - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/Route.kt b/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/Route.kt index e2207c6..376eb07 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/Route.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/navgraph/Route.kt @@ -1,7 +1,6 @@ package info.octera.droidstorybox.presentation.navgraph sealed class Route(val route: String) { - object OnBoardingScreen : Route(route = "onBoardingScreen") object HomeScreen : Route(route = "homeScreen") @@ -17,5 +16,4 @@ sealed class Route(val route: String) { object NewsNavigation : Route(route = "newsNavigation") object NewsNavigatorScreen : Route(route = "newsNavigator") - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/NewsNavigator.kt b/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/NewsNavigator.kt index bcc202f..76dffc5 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/NewsNavigator.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/NewsNavigator.kt @@ -37,34 +37,35 @@ import info.octera.droidstorybox.presentation.search.SearchViewModel @Composable fun NewsNavigator() { - - val bottomNavigationItems = remember { - listOf( - BottomNavigationItem(icon = R.drawable.baseline_home_24, text = "Home"), - BottomNavigationItem(icon = R.drawable.baseline_search_24, text = "Search"), - BottomNavigationItem(icon = R.drawable.baseline_bookmark_border_24, text = "Bookmark"), - ) - } + val bottomNavigationItems = + remember { + listOf( + BottomNavigationItem(icon = R.drawable.baseline_home_24, text = "Home"), + BottomNavigationItem(icon = R.drawable.baseline_search_24, text = "Search"), + BottomNavigationItem(icon = R.drawable.baseline_bookmark_border_24, text = "Bookmark"), + ) + } val navController = rememberNavController() val backStackState = navController.currentBackStackEntryAsState().value var selectedItem by rememberSaveable { mutableIntStateOf(0) } - selectedItem = when (backStackState?.destination?.route) { - Route.HomeScreen.route -> 0 - Route.SearchScreen.route -> 1 - Route.BookmarkScreen.route -> 2 - else -> 0 - } + selectedItem = + when (backStackState?.destination?.route) { + Route.HomeScreen.route -> 0 + Route.SearchScreen.route -> 1 + Route.BookmarkScreen.route -> 2 + else -> 0 + } - //Hide the bottom navigation when the user is in the details screen - val isBottomBarVisible = remember(key1 = backStackState) { - backStackState?.destination?.route == Route.HomeScreen.route || + // Hide the bottom navigation when the user is in the details screen + val isBottomBarVisible = + remember(key1 = backStackState) { + backStackState?.destination?.route == Route.HomeScreen.route || backStackState?.destination?.route == Route.SearchScreen.route || backStackState?.destination?.route == Route.BookmarkScreen.route - } - + } Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = { if (isBottomBarVisible) { @@ -73,22 +74,25 @@ fun NewsNavigator() { selectedItem = selectedItem, onItemClick = { index -> when (index) { - 0 -> navigateToTab( - navController = navController, - route = Route.HomeScreen.route - ) - - 1 -> navigateToTab( - navController = navController, - route = Route.SearchScreen.route - ) - - 2 -> navigateToTab( - navController = navController, - route = Route.BookmarkScreen.route - ) + 0 -> + navigateToTab( + navController = navController, + route = Route.HomeScreen.route, + ) + + 1 -> + navigateToTab( + navController = navController, + route = Route.SearchScreen.route, + ) + + 2 -> + navigateToTab( + navController = navController, + route = Route.BookmarkScreen.route, + ) } - } + }, ) } }) { @@ -96,7 +100,7 @@ fun NewsNavigator() { NavHost( navController = navController, startDestination = Route.HomeScreen.route, - modifier = Modifier.padding(bottom = bottomPadding) + modifier = Modifier.padding(bottom = bottomPadding), ) { composable(route = Route.HomeScreen.route) { backStackEntry -> val viewModel: HomeViewModel = hiltViewModel() @@ -106,15 +110,15 @@ fun NewsNavigator() { navigateToSearch = { navigateToTab( navController = navController, - route = Route.SearchScreen.route + route = Route.SearchScreen.route, ) }, navigateToDetails = { article -> navigateToDetails( navController = navController, - article = article + article = article, ) - } + }, ) } composable(route = Route.SearchScreen.route) { @@ -127,17 +131,18 @@ fun NewsNavigator() { navigateToDetails = { article -> navigateToDetails( navController = navController, - article = article + article = article, ) - } + }, ) } composable(route = Route.DetailsScreen.route) { val viewModel: DetailViewModel = hiltViewModel() - if(viewModel.sideEffect != null){ - Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT).show() - viewModel.onEvent(DetailsEvent.RemoveSideEffect) - } + if (viewModel.sideEffect != null) + { + Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT).show() + viewModel.onEvent(DetailsEvent.RemoveSideEffect) + } navController.previousBackStackEntry?.savedStateHandle?.get("article") ?.let { article -> DetailsScreen( @@ -146,7 +151,6 @@ fun NewsNavigator() { navigateUp = { navController.navigateUp() }, ) } - } composable(route = Route.BookmarkScreen.route) { val viewModel: BookmarkViewModel = hiltViewModel() @@ -157,9 +161,9 @@ fun NewsNavigator() { navigateToDetails = { article -> navigateToDetails( navController = navController, - article = article + article = article, ) - } + }, ) } } @@ -171,12 +175,15 @@ fun OnBackClickStateSaver(navController: NavController) { BackHandler(true) { navigateToTab( navController = navController, - route = Route.HomeScreen.route + route = Route.HomeScreen.route, ) } } -private fun navigateToTab(navController: NavController, route: String) { +private fun navigateToTab( + navController: NavController, + route: String, +) { navController.navigate(route) { navController.graph.startDestinationRoute?.let { screen_route -> popUpTo(screen_route) { @@ -188,25 +195,12 @@ private fun navigateToTab(navController: NavController, route: String) { } } -private fun navigateToDetails(navController: NavController, article: Article) { +private fun navigateToDetails( + navController: NavController, + article: Article, +) { navController.currentBackStackEntry?.savedStateHandle?.set("article", article) navController.navigate( - route = Route.DetailsScreen.route + route = Route.DetailsScreen.route, ) } - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/components/NewsBottomNavigation.kt b/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/components/NewsBottomNavigation.kt index b4b9fd2..7378a1f 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/components/NewsBottomNavigation.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/news_navigation/components/NewsBottomNavigation.kt @@ -24,17 +24,16 @@ import info.octera.droidstorybox.R import info.octera.droidstorybox.presentation.Dimens.ExtraSmallPadding2 import info.octera.droidstorybox.ui.theme.NewsAppTheme - @Composable fun NewsBottomNavigation( items: List, selectedItem: Int, - onItemClick: (Int) -> Unit + onItemClick: (Int) -> Unit, ) { NavigationBar( modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.background, - tonalElevation = 10.dp + tonalElevation = 10.dp, ) { items.forEachIndexed { index, item -> NavigationBarItem( @@ -51,13 +50,14 @@ fun NewsBottomNavigation( Text(text = item.text, style = MaterialTheme.typography.labelSmall) } }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = colorResource(id = R.color.body), - unselectedTextColor = colorResource(id = R.color.body), - indicatorColor = MaterialTheme.colorScheme.background - ), + colors = + NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = colorResource(id = R.color.body), + unselectedTextColor = colorResource(id = R.color.body), + indicatorColor = MaterialTheme.colorScheme.background, + ), ) } } @@ -65,19 +65,23 @@ fun NewsBottomNavigation( data class BottomNavigationItem( @DrawableRes val icon: Int, - val text: String + val text: String, ) - @Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable fun NewsBottomNavigationPreview() { NewsAppTheme(dynamicColor = false) { - NewsBottomNavigation(items = listOf( - BottomNavigationItem(icon = R.drawable.baseline_home_24, text = "Home"), - BottomNavigationItem(icon = R.drawable.baseline_search_24, text = "Search"), - BottomNavigationItem(icon = R.drawable.baseline_bookmark_border_24, text = "Bookmark"), - ), selectedItem = 0, onItemClick = {}) + NewsBottomNavigation( + items = + listOf( + BottomNavigationItem(icon = R.drawable.baseline_home_24, text = "Home"), + BottomNavigationItem(icon = R.drawable.baseline_search_24, text = "Search"), + BottomNavigationItem(icon = R.drawable.baseline_bookmark_border_24, text = "Bookmark"), + ), + selectedItem = 0, + onItemClick = {}, + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingEvent.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingEvent.kt index f95b893..f52e93a 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingEvent.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingEvent.kt @@ -1,7 +1,5 @@ package info.octera.droidstorybox.presentation.onboarding -sealed class OnBoardingEvent { - +sealed class OnBoardingEvent { object SaveAppEntry : OnBoardingEvent() - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingViewModel.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingViewModel.kt index f6b5150..e2b38bf 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/OnBoardingViewModel.kt @@ -2,27 +2,28 @@ package info.octera.droidstorybox.presentation.onboarding import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import info.octera.droidstorybox.domain.usecases.app_entry.AppEntryUseCases import dagger.hilt.android.lifecycle.HiltViewModel +import info.octera.droidstorybox.domain.usecases.app_entry.AppEntryUseCases import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class OnBoardingViewModel @Inject constructor( - private val appEntryUseCases: AppEntryUseCases -): ViewModel() { - - fun onEvent(event: OnBoardingEvent) { - when(event) { - is OnBoardingEvent.SaveAppEntry -> { - saveAppEntry() - } +class OnBoardingViewModel + @Inject + constructor( + private val appEntryUseCases: AppEntryUseCases, + ) : ViewModel() { + fun onEvent(event: OnBoardingEvent) { + when (event) { + is OnBoardingEvent.SaveAppEntry -> { + saveAppEntry() + } + } } - } - private fun saveAppEntry() { - viewModelScope.launch { - appEntryUseCases.saveAppEntry() + private fun saveAppEntry() { + viewModelScope.launch { + appEntryUseCases.saveAppEntry() + } } } -} \ No newline at end of file diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/Page.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/Page.kt index 2a0144f..b0d2553 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/Page.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/Page.kt @@ -5,27 +5,25 @@ import info.octera.droidstorybox.R data class Page( val title: String, - val description : String, - @DrawableRes val image: Int + val description: String, + @DrawableRes val image: Int, ) - -val pages = listOf( - Page( - title = "Welcome to NewsConnect!", - description = "Stay informed with the latest news from around the world. NewsConnect brings you real-time updates on current events, politics, sports, entertainment, and more.", - image = R.drawable.onboarding1 - ), - - Page( - title = "Stay Updated, Anywhere, Anytime", - description = "Get notifications on breaking news and trending stories. With NewsConnect, you'll never miss an important update, whether you're at home or on the go.", - image = R.drawable.onboarding2 - ), - Page( - title = "Get Started with NewsConnect", - description = "Dive into a world of news tailored just for you. Tap the button below to begin exploring!", - image = R.drawable.onboarding3 +val pages = + listOf( + Page( + title = "Welcome to NewsConnect!", + description = "Stay informed with the latest news from around the world. NewsConnect brings you real-time updates on current events, politics, sports, entertainment, and more.", + image = R.drawable.onboarding1, + ), + Page( + title = "Stay Updated, Anywhere, Anytime", + description = "Get notifications on breaking news and trending stories. With NewsConnect, you'll never miss an important update, whether you're at home or on the go.", + image = R.drawable.onboarding2, + ), + Page( + title = "Get Started with NewsConnect", + description = "Dive into a world of news tailored just for you. Tap the button below to begin exploring!", + image = R.drawable.onboarding3, + ), ) - -) \ No newline at end of file diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingPage.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingPage.kt index afd56b5..1606c09 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingPage.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingPage.kt @@ -20,32 +20,33 @@ import info.octera.droidstorybox.presentation.Dimens.MediumPadding1 import info.octera.droidstorybox.presentation.Dimens.MediumPadding2 import info.octera.droidstorybox.presentation.onboarding.Page - @Composable -fun OnBoardingPage(modifier: Modifier = Modifier, page: Page) { +fun OnBoardingPage( + modifier: Modifier = Modifier, + page: Page, +) { Column(modifier = modifier) { Image( painter = painterResource(id = page.image), contentDescription = "", - modifier = modifier - .fillMaxWidth() - .fillMaxHeight(0.6f), - contentScale = ContentScale.Crop + modifier = + modifier + .fillMaxWidth() + .fillMaxHeight(0.6f), + contentScale = ContentScale.Crop, ) Spacer(modifier = modifier.height(MediumPadding1)) Text( modifier = modifier.padding(horizontal = MediumPadding2), text = page.title, style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), - color = colorResource(id = R.color.display_small) + color = colorResource(id = R.color.display_small), ) Text( modifier = modifier.padding(horizontal = MediumPadding2), text = page.description, style = MaterialTheme.typography.bodyMedium, - color = colorResource(id = R.color.text_medium) + color = colorResource(id = R.color.text_medium), ) } - } - diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingScreen.kt index 532ad92..09a757d 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/OnBoardingScreen.kt @@ -19,9 +19,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import info.octera.droidstorybox.presentation.Dimens.IndicatorSize import info.octera.droidstorybox.presentation.common.NewsButton import info.octera.droidstorybox.presentation.common.NewsTextButton -import info.octera.droidstorybox.presentation.Dimens.IndicatorSize import info.octera.droidstorybox.presentation.onboarding.OnBoardingEvent import info.octera.droidstorybox.presentation.onboarding.pages import kotlinx.coroutines.launch @@ -29,22 +29,25 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun OnBoardingScreen( - modifier: Modifier = Modifier, event: (OnBoardingEvent) -> Unit + modifier: Modifier = Modifier, + event: (OnBoardingEvent) -> Unit, ) { Column(modifier = modifier.fillMaxSize()) { - val pagerState = rememberPagerState(initialPage = 0) { - pages.size - } - val buttonState = remember { - derivedStateOf { - when (pagerState.currentPage) { - 0 -> listOf("", "Next") - 1 -> listOf("Back", "Next") - 2 -> listOf("Back", "Get Started") - else -> listOf("", "") + val pagerState = + rememberPagerState(initialPage = 0) { + pages.size + } + val buttonState = + remember { + derivedStateOf { + when (pagerState.currentPage) { + 0 -> listOf("", "Next") + 1 -> listOf("Back", "Next") + 2 -> listOf("Back", "Get Started") + else -> listOf("", "") + } } } - } HorizontalPager(state = pagerState) { index -> OnBoardingPage(page = pages[index]) @@ -52,22 +55,23 @@ fun OnBoardingScreen( Spacer(modifier = Modifier.weight(1f)) Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 10.dp) - .navigationBarsPadding(), + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + .navigationBarsPadding(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { PageIndicator( pageSize = pages.size, selectedPage = pagerState.currentPage, - modifier = Modifier.width(IndicatorSize) + modifier = Modifier.width(IndicatorSize), ) Row( modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { val scope = rememberCoroutineScope() @@ -88,11 +92,8 @@ fun OnBoardingScreen( } } }) - } - } Spacer(modifier = Modifier.weight(0.5f)) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/PageIndicator.kt b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/PageIndicator.kt index ec72e04..4719e09 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/PageIndicator.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/onboarding/components/PageIndicator.kt @@ -18,25 +18,26 @@ import info.octera.droidstorybox.ui.theme.BlueGray @Composable fun PageIndicator( - modifier: Modifier = Modifier, pageSize: Int, + modifier: Modifier = Modifier, + pageSize: Int, selectedPage: Int, selectedColor: Color = MaterialTheme.colorScheme.primary, - unselectedColor: Color = BlueGray - + unselectedColor: Color = BlueGray, ) { Row( modifier = Modifier.padding(horizontal = 4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { repeat(pageSize) { page -> Box( - modifier = modifier - .size(24.dp) - .padding(vertical = 4.dp, horizontal = 2.dp) - .clip(CircleShape) - .background(color = if (page == selectedPage) selectedColor else unselectedColor) + modifier = + modifier + .size(24.dp) + .padding(vertical = 4.dp, horizontal = 2.dp) + .clip(CircleShape) + .background(color = if (page == selectedPage) selectedColor else unselectedColor), ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchEvent.kt b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchEvent.kt index 70aa0d2..a1e220c 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchEvent.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchEvent.kt @@ -1,9 +1,7 @@ package info.octera.droidstorybox.presentation.search - sealed class SearchEvent { - data class UpdateSearchQuery(val searchQuery: String) : SearchEvent() data object SearchNews : SearchEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchScreen.kt b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchScreen.kt index 7245a7a..cd27184 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchScreen.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchScreen.kt @@ -16,15 +16,17 @@ import info.octera.droidstorybox.presentation.common.SearchBar @Composable fun SearchScreen( - modifier: Modifier = Modifier, state: SearchState, - event: (SearchEvent) -> Unit, navigateToDetails: (Article) -> Unit + modifier: Modifier = Modifier, + state: SearchState, + event: (SearchEvent) -> Unit, + navigateToDetails: (Article) -> Unit, ) { - Column( - modifier = modifier - .padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1) - .statusBarsPadding() - .fillMaxSize() + modifier = + modifier + .padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1) + .statusBarsPadding() + .fillMaxSize(), ) { SearchBar( text = state.searchQuery, @@ -32,7 +34,7 @@ fun SearchScreen( onValueChange = { event(SearchEvent.UpdateSearchQuery(it)) }, onSearch = { event(SearchEvent.SearchNews) - } + }, ) Spacer(modifier = modifier.height(MediumPadding1)) state.articles?.let { @@ -40,9 +42,9 @@ fun SearchScreen( ArticlesList( articles = articles, onClick = { - navigateToDetails(it) - } + navigateToDetails(it) + }, ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchState.kt b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchState.kt index e31e81c..70433b5 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchState.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchState.kt @@ -6,6 +6,5 @@ import kotlinx.coroutines.flow.Flow data class SearchState( val searchQuery: String = "", - val articles : Flow>? = null -) { -} \ No newline at end of file + val articles: Flow>? = null, +) diff --git a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchViewModel.kt b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchViewModel.kt index 8922b74..1e449d5 100644 --- a/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchViewModel.kt +++ b/app/src/main/java/info/octera/droidstorybox/presentation/search/SearchViewModel.kt @@ -5,35 +5,37 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn -import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import dagger.hilt.android.lifecycle.HiltViewModel +import info.octera.droidstorybox.domain.usecases.news.NewsUseCases import javax.inject.Inject @HiltViewModel -class SearchViewModel @Inject constructor( - private val newsUseCases: NewsUseCases -) : ViewModel() { - - private val _state = mutableStateOf(SearchState()) - val state: State = _state +class SearchViewModel + @Inject + constructor( + private val newsUseCases: NewsUseCases, + ) : ViewModel() { + private val _state = mutableStateOf(SearchState()) + val state: State = _state - fun onEvent(event: SearchEvent) { - when (event) { - is SearchEvent.UpdateSearchQuery -> { - _state.value = state.value.copy(searchQuery = event.searchQuery) - } + fun onEvent(event: SearchEvent) { + when (event) { + is SearchEvent.UpdateSearchQuery -> { + _state.value = state.value.copy(searchQuery = event.searchQuery) + } - is SearchEvent.SearchNews -> { - searchNews() + is SearchEvent.SearchNews -> { + searchNews() + } } } - } - private fun searchNews() { - val articles = newsUseCases.searchNews( - searchQuery = _state.value.searchQuery, - sources = listOf("bbc-news", "abc-news", "al-jazeera-english") - ).cachedIn(viewModelScope) - _state.value = state.value.copy(articles = articles) + private fun searchNews() { + val articles = + newsUseCases.searchNews( + searchQuery = _state.value.searchQuery, + sources = listOf("bbc-news", "abc-news", "al-jazeera-english"), + ).cachedIn(viewModelScope) + _state.value = state.value.copy(articles = articles) + } } -} \ No newline at end of file diff --git a/app/src/main/java/info/octera/droidstorybox/ui/theme/Color.kt b/app/src/main/java/info/octera/droidstorybox/ui/theme/Color.kt index c170d80..1466982 100644 --- a/app/src/main/java/info/octera/droidstorybox/ui/theme/Color.kt +++ b/app/src/main/java/info/octera/droidstorybox/ui/theme/Color.kt @@ -2,14 +2,13 @@ package info.octera.droidstorybox.ui.theme import androidx.compose.ui.graphics.Color -val Black = Color(0xFF1C1E21) //Dark Background -val Blue = Color(0xFF1877F2) //Primary +val Black = Color(0xFF1C1E21) // Dark Background +val Blue = Color(0xFF1877F2) // Primary -val DarkRed = Color(0xFFC30052) //Dark Error +val DarkRed = Color(0xFFC30052) // Dark Error val LightRed = Color(0xFFFF84B7) -val LightBlack = Color(0xFF3A3B3C) //Dark Surface - +val LightBlack = Color(0xFF3A3B3C) // Dark Surface val BlueGray = Color(0xFFA0A3BD) -val WhiteGray = Color(0xFFB0B3B8) \ No newline at end of file +val WhiteGray = Color(0xFFB0B3B8) diff --git a/app/src/main/java/info/octera/droidstorybox/ui/theme/Theme.kt b/app/src/main/java/info/octera/droidstorybox/ui/theme/Theme.kt index 2799c3e..9087eec 100644 --- a/app/src/main/java/info/octera/droidstorybox/ui/theme/Theme.kt +++ b/app/src/main/java/info/octera/droidstorybox/ui/theme/Theme.kt @@ -11,19 +11,21 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Blue, - background = Black, - error = DarkRed, - surface = LightBlack -) +private val DarkColorScheme = + darkColorScheme( + primary = Blue, + background = Black, + error = DarkRed, + surface = LightBlack, + ) -private val LightColorScheme = lightColorScheme( - primary = Blue, - background = Color.White, - error = LightRed, - surface = Color.White -) +private val LightColorScheme = + lightColorScheme( + primary = Blue, + background = Color.White, + error = LightRed, + surface = Color.White, + ) /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), @@ -32,29 +34,29 @@ private val LightColorScheme = lightColorScheme( onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), - */ - + */ @Composable fun NewsAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/info/octera/droidstorybox/ui/theme/Type.kt b/app/src/main/java/info/octera/droidstorybox/ui/theme/Type.kt index 941f558..01be71c 100644 --- a/app/src/main/java/info/octera/droidstorybox/ui/theme/Type.kt +++ b/app/src/main/java/info/octera/droidstorybox/ui/theme/Type.kt @@ -7,14 +7,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -30,5 +32,5 @@ val Typography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) - */ -) \ No newline at end of file + */ + ) diff --git a/app/src/main/java/info/octera/droidstorybox/util/Constants.kt b/app/src/main/java/info/octera/droidstorybox/util/Constants.kt index ee9daa1..20d44de 100644 --- a/app/src/main/java/info/octera/droidstorybox/util/Constants.kt +++ b/app/src/main/java/info/octera/droidstorybox/util/Constants.kt @@ -6,4 +6,4 @@ object Constants { const val API_KEY = "fc4d356d6ec8482a8a788443d262cf10" const val BASE_URL = "https://newsapi.org/v2/" const val NEW_DATABASE = "news_db" -} \ No newline at end of file +} From fa92aae5b269c100856625f74e35c0aab278f8a5 Mon Sep 17 00:00:00 2001 From: ROGER Mathieu Date: Tue, 30 Jul 2024 21:03:12 +0200 Subject: [PATCH 4/4] chore(ci) : disable fastlane (will see later how to release the app) --- .github/workflows/develop-release-ci.yml | 8 +------- .github/workflows/master-release-ci.yml | 8 -------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/develop-release-ci.yml b/.github/workflows/develop-release-ci.yml index dc70d80..d38c85f 100644 --- a/.github/workflows/develop-release-ci.yml +++ b/.github/workflows/develop-release-ci.yml @@ -43,10 +43,4 @@ jobs: # Step 3: Check the code with Android linter - name: Run Android Linter - run: ./gradlew lintDebug - - name: Install bundle - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - name: Attempt to test and build app throught fastlane - run: bundle exec fastlane test \ No newline at end of file + run: ./gradlew lintDebug \ No newline at end of file diff --git a/.github/workflows/master-release-ci.yml b/.github/workflows/master-release-ci.yml index 1207add..a592d5a 100644 --- a/.github/workflows/master-release-ci.yml +++ b/.github/workflows/master-release-ci.yml @@ -45,11 +45,3 @@ jobs: base64 -d -i play_config.json.b64 > api-service-account.json env: PLAY_CONFIG_JSON: ${{ secrets.PLAY_CONFIG_JSON }} - - name: Install bundle - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - name: Attempt to test and build app throught fastlane - run: bundle exec fastlane test - - name: Distribute app to Alpha track 🚀 - run: bundle exec fastlane alpha \ No newline at end of file