diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..88e9dba --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Newzz-Compose \ No newline at end of file diff --git a/.idea/codeStyles b/.idea/codeStyles new file mode 100644 index 0000000..ad98bfd --- /dev/null +++ b/.idea/codeStyles @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/nbt762.xml b/.idea/dictionaries/nbt762.xml new file mode 100644 index 0000000..6ba15ee --- /dev/null +++ b/.idea/dictionaries/nbt762.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b9f8a5e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3fb6130 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,94 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "com.akash.newzz_compose" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + def apiKey = properties.getProperty("api_key") + buildConfigField("String", "API_KEY", apiKey) + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + + composeOptions { + kotlinCompilerVersion "1.3.70-dev-withExperimentalGoogleExtensions-20200424" + kotlinCompilerExtensionVersion "0.1.0-dev12" + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + + /* ---------------------JetPack Compose-----------------------------*/ + implementation("androidx.compose:compose-runtime:$compose_version") + implementation("androidx.ui:ui-core:$compose_version") + implementation "androidx.ui:ui-layout:$compose_version" + implementation "androidx.ui:ui-material:$compose_version" + implementation "androidx.ui:ui-tooling:$compose_version" + implementation "androidx.ui:ui-livedata:$compose_version" + + /* ---------------------JetPack Compose-----------------------------*/ + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + + + /* ---------------------Kotlin-coroutines---------------------------*/ + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + + /*---------------------Network and Moshi----------------------------*/ + implementation 'com.squareup.retrofit2:retrofit:2.6.2' + implementation 'com.squareup.okhttp3:okhttp:4.2.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' + implementation 'com.squareup.moshi:moshi:1.9.1' + kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.1' + + + /*-----------------------Koin-------------------------------------*/ + implementation "org.koin:koin-android:$koin_version" + implementation "org.koin:koin-android-viewmodel:$koin_version" + + /*-----------------------Glide-----------------------------*/ + implementation 'com.github.bumptech.glide:glide:4.11.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + + /*---------------------ChromeCustomTab------------------------------*/ + implementation 'androidx.browser:browser:1.3.0-alpha01' + + /*---------------------CoilImageLoader------------------------------*/ + implementation "dev.chrisbanes.accompanist:accompanist-coil:0.1.3" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/akash/newzz_compose/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/akash/newzz_compose/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b7c1975 --- /dev/null +++ b/app/src/androidTest/java/com/akash/newzz_compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.akash.newzz_compose + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.akash.newzz_compose", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..490acac --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/Category.kt b/app/src/main/java/com/akash/newzz_compose/Category.kt new file mode 100644 index 0000000..55c7e99 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/Category.kt @@ -0,0 +1,17 @@ +package com.akash.newzz_compose + +import androidx.annotation.StringDef + +/** + * Created by Akash on 06/06/20 + */ + +@Retention(AnnotationRetention.SOURCE) +@StringDef(value = [Category.GENERAL, Category.BUSINESS, Category.TECH]) +internal annotation class Category { + companion object { + const val GENERAL = "general" + const val BUSINESS = "business" + const val TECH = "technology" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/Constants.kt b/app/src/main/java/com/akash/newzz_compose/Constants.kt new file mode 100644 index 0000000..933582c --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/Constants.kt @@ -0,0 +1,11 @@ +package com.akash.newzz_compose + +/** + * Created by Akash on 06/06/20 + */ +object Constants { + val API_KEY: String + get() = BuildConfig.API_KEY + val COUNTRY: String + get() = "in" +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/NewsApplication.kt b/app/src/main/java/com/akash/newzz_compose/NewsApplication.kt new file mode 100644 index 0000000..f092d72 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/NewsApplication.kt @@ -0,0 +1,38 @@ +package com.akash.newzz_compose + +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import com.akash.newzz_compose.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +/** + * Created by Akash on 06/06/20 + */ +class NewsApplication : Application() { + + override fun onCreate() { + super.onCreate() + instances = this + startKoin{ + androidLogger() + androidContext(this@NewsApplication) + modules(appModule) + } + } + + + companion object { + lateinit var instances: NewsApplication + + + fun isNetworkConnected(): Boolean { + val connectivityManager = + instances.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = connectivityManager.activeNetworkInfo + return networkInfo != null && networkInfo.isConnected + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/apiService/NewsApiService.kt b/app/src/main/java/com/akash/newzz_compose/data/apiService/NewsApiService.kt new file mode 100644 index 0000000..7f233a6 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/apiService/NewsApiService.kt @@ -0,0 +1,109 @@ +package com.akash.newzz_compose.data.apiService + +/** + * Created by Akash on 06/06/20 + */ +import com.akash.newzz_compose.Constants +import com.akash.newzz_compose.NewsApplication +import com.akash.newzz_compose.data.response.NewsResponse +import okhttp3.Cache +import okhttp3.CacheControl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET +import retrofit2.http.Query +import java.io.File +import java.util.concurrent.TimeUnit + +interface NewsApiService { + + + @GET("top-headlines?sortBy=publishedAt&pageSize=25") + suspend fun getArticlesByCateGoryAsync( + @Query("category") category: String, + @Query("country") country: String = Constants.COUNTRY + ): Response + + + companion object { + + private const val HEADER_CACHE_CONTROL = "Cache-Control" + private const val HEADER_PRAGMA = "Pragma" + private const val cacheSize = (5 * 1024 * 1024).toLong() // 5 MB + + + operator fun invoke(): NewsApiService { + val requestInterceptor = Interceptor { chain -> + + val response = chain.proceed(chain.request()) + + val cacheControl = CacheControl.Builder() + .maxAge(5, TimeUnit.SECONDS) + .build() + + return@Interceptor response.newBuilder() + .removeHeader(HEADER_PRAGMA) + .removeHeader(HEADER_CACHE_CONTROL) + .header(HEADER_CACHE_CONTROL, cacheControl.toString()) + .build() + } + + val cache = Cache( + File(NewsApplication.instances.cacheDir, "networkCache"), + cacheSize + ) + + val offlineInterceptor = Interceptor { chain -> + + + val url = chain.request() + .url + .newBuilder() + .addQueryParameter("apiKey", Constants.API_KEY) + .build() + + var request = chain.request() + + if (NewsApplication.isNetworkConnected()) { + + request = request + .newBuilder() + .url(url) + .build() + } else { + + val cacheControl = CacheControl.Builder() + .maxStale(7, TimeUnit.DAYS) + .build() + + request = request + .newBuilder() + .url(url) + .removeHeader(HEADER_PRAGMA) + .removeHeader(HEADER_CACHE_CONTROL) + .cacheControl(cacheControl) + .build() + + } + + return@Interceptor chain.proceed(request) + } + + val okHttpClient = OkHttpClient.Builder() + .cache(cache) + .addInterceptor(offlineInterceptor) + .addNetworkInterceptor(requestInterceptor) + .build() + + return Retrofit.Builder() + .baseUrl("https://newsapi.org/v2/") + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create()) + .build() + .create(NewsApiService::class.java) + } + } +} diff --git a/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepository.kt b/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepository.kt new file mode 100644 index 0000000..a1ca2de --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepository.kt @@ -0,0 +1,11 @@ +package com.akash.newzz_compose.data.repository + +import com.akash.newzz_compose.data.response.NewsResponse +import com.akash.newzz_compose.utils.Result + +/** + * Created by Akash on 06/06/20 + */ +interface NewsRepository { + suspend fun getArticlesByCategoryAsync(category: String, page: Int): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepositoryImpl.kt b/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepositoryImpl.kt new file mode 100644 index 0000000..9c0e198 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/repository/NewsRepositoryImpl.kt @@ -0,0 +1,57 @@ +package com.akash.newzz_compose.data.repository + +import com.akash.newzz_compose.data.apiService.NewsApiService +import com.akash.newzz_compose.data.response.NewsError +import com.akash.newzz_compose.data.response.NewsResponse +import com.akash.newzz_compose.utils.Result +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Created by Akash on 06/06/20 + */ + +class NewsRepositoryImpl constructor( + private val newsApiService: NewsApiService, + private val moshi: Moshi +) : NewsRepository { + override suspend fun getArticlesByCategoryAsync(category: String, page: Int): Result { + try { + val response = newsApiService.getArticlesByCateGoryAsync(category) + return if (response.isSuccessful) { + if (response.body() != null) { + Result.Success(response.body()!!) + } else { + Result.Error("No Data found") + } + } else { + val jsonAdapter: JsonAdapter = moshi.adapter( + NewsError::class.java + ) + withContext(Dispatchers.IO) { + val newsError = jsonAdapter.fromJson(response.errorBody()?.string()!!) + Result.Error( + newsError!!.message, + showRetry(newsError.code) + ) + } + } + + } catch (e: Exception) { + e.printStackTrace() + var errorMessage = e.localizedMessage + if (e.localizedMessage!!.contains("Unable to resolve host")) { + errorMessage = "No internet connection" + } + return Result.Error(errorMessage ?: "Something went wrong") + } + } + + private fun showRetry(code: String): Boolean = when (code) { + "apiKeyDisabled", "apiKeyExhausted", "apiKeyInvalid", "apiKeyMissing" -> false + else -> true + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/response/NewsArticle.kt b/app/src/main/java/com/akash/newzz_compose/data/response/NewsArticle.kt new file mode 100644 index 0000000..c7df98a --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/response/NewsArticle.kt @@ -0,0 +1,22 @@ +package com.akash.newzz_compose.data.response + +import com.squareup.moshi.JsonClass + +/** + * Created by Akash on 06/06/20 + */ + +@JsonClass(generateAdapter = true) +data class NewsArticle( + val publishedAt: String, + + val source: NewsSource, + + val title: String, + + val description: String? = null, + + val url: String? = null, + + val urlToImage: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/response/NewsError.kt b/app/src/main/java/com/akash/newzz_compose/data/response/NewsError.kt new file mode 100644 index 0000000..9db5321 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/response/NewsError.kt @@ -0,0 +1,16 @@ +package com.akash.newzz_compose.data.response + +import com.squareup.moshi.JsonClass + +/** + * Created by Akash on 06/06/20 + */ + +@JsonClass(generateAdapter = true) +data class NewsError( + val code: String, + + val message: String, + + val status: String +) \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/response/NewsResponse.kt b/app/src/main/java/com/akash/newzz_compose/data/response/NewsResponse.kt new file mode 100644 index 0000000..2183491 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/response/NewsResponse.kt @@ -0,0 +1,15 @@ +package com.akash.newzz_compose.data.response + +import com.squareup.moshi.JsonClass + +/** + * Created by Akash on 06/06/20 + */ +@JsonClass(generateAdapter = true) +data class NewsResponse( + val articles: List, + + var status: String, + + val totalResults: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/data/response/NewsSource.kt b/app/src/main/java/com/akash/newzz_compose/data/response/NewsSource.kt new file mode 100644 index 0000000..944fe86 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/data/response/NewsSource.kt @@ -0,0 +1,12 @@ +package com.akash.newzz_compose.data.response + +import com.squareup.moshi.JsonClass + +/** + * Created by Akash on 06/06/20 + */ + +@JsonClass(generateAdapter = true) +data class NewsSource( + val name: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/di/module.kt b/app/src/main/java/com/akash/newzz_compose/di/module.kt new file mode 100644 index 0000000..4800981 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/di/module.kt @@ -0,0 +1,20 @@ +package com.akash.newzz_compose.di + +import com.akash.newzz_compose.viewmodel.NewzzViewModel +import com.akash.newzz_compose.data.apiService.NewsApiService +import com.akash.newzz_compose.data.repository.NewsRepository +import com.akash.newzz_compose.data.repository.NewsRepositoryImpl +import com.squareup.moshi.Moshi +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.dsl.module + +/** + * Created by Akash on 06/06/20 + */ + +val appModule = module { + viewModel { NewzzViewModel(get()) } + single { NewsRepositoryImpl(get(), get()) as NewsRepository } + single { NewsApiService() } + single { Moshi.Builder().build() } +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/style/Color.kt b/app/src/main/java/com/akash/newzz_compose/style/Color.kt new file mode 100644 index 0000000..06344f7 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/style/Color.kt @@ -0,0 +1,34 @@ +package com.akash.newzz_compose.style + +import androidx.ui.graphics.Color + +/** + * Created by Akash on 07/06/20 + */ + +val deepPurple = Color(0xFF4a148c) +val darkColor = Color(0xFF121212) + +val sourceTextColor = Color(0xFF474646) +val sourceTextColorDark = Color(0xFF868080) + +val titleColor = Color.Black +val titleColorDark = Color(0xB3DCDCDC) + +val listBackgroundColor = Color.White +val listBackgroundColorDark = Color(0xFF1E1E1E) + +val bottomNavBackground = Color.White +val bottomNavBackgroundDark = Color(0xFF222222) + +val bottomNavIconActiveColor = Color(0xFF4a148c) +val bottomNavIconInActiveColor = Color.Black + +val bottomNavIconActiveColorDark = Color(0xB3DCDCDC) +val bottomNavIconInActiveColorDark = Color(0xFF5C5757) + +val circularLoaderColor = deepPurple +val circularLoaderColorDark = Color.White + +val dividerColor = Color(0xFFDCDCDC) +val dividerColorDark = Color(0xFF2B2929) diff --git a/app/src/main/java/com/akash/newzz_compose/style/TextStyle.kt b/app/src/main/java/com/akash/newzz_compose/style/TextStyle.kt new file mode 100644 index 0000000..7c42485 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/style/TextStyle.kt @@ -0,0 +1,34 @@ +package com.akash.newzz_compose.style + +import androidx.ui.graphics.Color +import androidx.ui.text.TextStyle +import androidx.ui.text.font.FontWeight +import androidx.ui.unit.sp + +/** + * Created by Akash on 07/06/20 + */ + + +val categoryTitleStyle = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = Color.White +) + +val articleTitleStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + color = titleColor +) + +val sourceTextStyle = TextStyle( + fontSize = 14.sp, + color = sourceTextColor +) + +val dateTextStyle = TextStyle( + fontWeight = FontWeight.ExtraLight, + fontSize = 12.sp, + color = sourceTextColor +) \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/style/Theme.kt b/app/src/main/java/com/akash/newzz_compose/style/Theme.kt new file mode 100644 index 0000000..202953d --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/style/Theme.kt @@ -0,0 +1,25 @@ +package com.akash.newzz_compose.style + +import androidx.ui.material.darkColorPalette +import androidx.ui.material.lightColorPalette + +/** + * Created by Akash on 05/06/20 + */ + +val themeColor = lightColorPalette( + primary = deepPurple, + primaryVariant = deepPurple, + secondary = deepPurple, + secondaryVariant = deepPurple, + surface = deepPurple, + background = deepPurple +) + +val darkThemeColor = darkColorPalette( + primary = darkColor, + primaryVariant = darkColor, + secondary = darkColor, + surface = darkColor +) + diff --git a/app/src/main/java/com/akash/newzz_compose/ui/NewzzActivity.kt b/app/src/main/java/com/akash/newzz_compose/ui/NewzzActivity.kt new file mode 100644 index 0000000..219adaf --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/NewzzActivity.kt @@ -0,0 +1,28 @@ +package com.akash.newzz_compose.ui + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.Model +import androidx.ui.core.setContent +import androidx.ui.livedata.observeAsState +import androidx.ui.material.MaterialTheme +import com.akash.newzz_compose.style.darkThemeColor +import com.akash.newzz_compose.style.themeColor +import com.akash.newzz_compose.viewmodel.NewzzViewModel +import com.akash.newzz_compose.ui.home.Home +import org.koin.android.viewmodel.ext.android.viewModel + + +class NewzzActivity : AppCompatActivity() { + private val viewModel: NewzzViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val isDark = viewModel.isDarkTheme.observeAsState(false) + MaterialTheme(colors = if(isDark.value) darkThemeColor else themeColor) { + Home(viewModel) + } + } + } +} diff --git a/app/src/main/java/com/akash/newzz_compose/ui/articleListPage/ArticleListScreen.kt b/app/src/main/java/com/akash/newzz_compose/ui/articleListPage/ArticleListScreen.kt new file mode 100644 index 0000000..cca53cc --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/articleListPage/ArticleListScreen.kt @@ -0,0 +1,104 @@ +package com.akash.newzz_compose.ui.articleListPage + +import androidx.compose.Composable +import androidx.compose.State +import androidx.ui.core.Alignment +import androidx.ui.core.ContextAmbient +import androidx.ui.core.Modifier +import androidx.ui.foundation.Text +import androidx.ui.foundation.VerticalScroller +import androidx.ui.foundation.clickable +import androidx.ui.layout.* +import androidx.ui.material.Divider +import androidx.ui.text.style.TextOverflow +import androidx.ui.unit.dp +import com.akash.newzz_compose.data.response.NewsArticle +import com.akash.newzz_compose.style.* +import com.akash.newzz_compose.ui.common.HeightSpacer +import com.akash.newzz_compose.ui.common.RemoteImage +import com.akash.newzz_compose.ui.common.WidthSpacer +import com.akash.newzz_compose.utils.CustomTabUtil + +/** + * Created by Akash on 06/06/20 + */ + +@Composable +fun ArticleRow(article: NewsArticle, isDark: State, onClick: () -> Unit) { + Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)) { + Row( + /* onClick with more than one modifier was not working hence wrapping it with Column + * Tried modifier = Modifier.clickable( + onClick = { + onClick() + } + ).plus(Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)) + */ + modifier = Modifier.clickable( + onClick = { + onClick() + } + ), + verticalGravity = Alignment.CenterVertically + ) { + RemoteImage( + url = article.urlToImage, + modifier = Modifier.preferredHeight(100.dp) + .plus(Modifier.preferredWidth(100.dp)) + ) + WidthSpacer(value = 10.dp) + Column { + if (!article.source.name.isNullOrEmpty()) { + Text( + text = article.source.name, + style = if (isDark.value) sourceTextStyle.copy(color = sourceTextColorDark) else sourceTextStyle + ) + HeightSpacer(value = 4.dp) + } + Text( + text = article.title, + style = if (isDark.value) articleTitleStyle.copy(color = titleColorDark) else articleTitleStyle, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + HeightSpacer(value = 4.dp) + Text( + text = article.publishedAt.substring(0, 10), + style = if (isDark.value) dateTextStyle.copy(color = sourceTextColorDark) else dateTextStyle + ) + } + } + } +} + +@Composable +fun ArticleList(articles: List, isDark: State) { + /** + * App crashing while scrolling the AdapterList hence verticalScroller with Columns used. + * @see Issue tracker + */ + /*AdapterList(data = articles) { article -> + ArticleRow(article = article) + Divider( + color = Color(0xFFDCDCDC) + ) + }*/ + val context = ContextAmbient.current + VerticalScroller { + Column(modifier = Modifier.fillMaxWidth()) { + articles.forEach { article -> + ArticleRow( + article = article, + isDark = isDark, + onClick = { + CustomTabUtil.launch(context, article.url.toString(), isDark.value) + } + ) + HeightSpacer(value = 10.dp) + Divider( + color = if (isDark.value) dividerColorDark else dividerColor + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/ui/common/Common.kt b/app/src/main/java/com/akash/newzz_compose/ui/common/Common.kt new file mode 100644 index 0000000..7e018d5 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/common/Common.kt @@ -0,0 +1,66 @@ +package com.akash.newzz_compose.ui.common + +import androidx.compose.Composable +import androidx.ui.core.ContentScale +import androidx.ui.core.Modifier +import androidx.ui.foundation.Box +import androidx.ui.foundation.Image +import androidx.ui.foundation.shape.corner.RoundedCornerShape +import androidx.ui.graphics.Color +import androidx.ui.graphics.Shape +import androidx.ui.graphics.vector.VectorAsset +import androidx.ui.layout.Spacer +import androidx.ui.layout.fillMaxSize +import androidx.ui.layout.preferredHeight +import androidx.ui.layout.preferredWidth +import androidx.ui.material.Surface +import androidx.ui.res.vectorResource +import androidx.ui.unit.Dp +import androidx.ui.unit.dp +import com.akash.newzz_compose.R +import dev.chrisbanes.accompanist.coil.CoilImageWithCrossfade + +/** + * Created by Akash on 07/06/20 + */ + +@Composable +fun HeightSpacer(value: Dp) { + Spacer(modifier = Modifier.preferredHeight(value)) +} + +@Composable +fun WidthSpacer(value: Dp) { + Spacer(modifier = Modifier.preferredWidth(value)) +} + +@Composable +fun RemoteImage( + url: String?, + modifier: Modifier, + errorImage: VectorAsset = vectorResource(id = R.drawable.ic_newzz_error), + contentScale: ContentScale = ContentScale.Crop, + shape : Shape = RoundedCornerShape(5.dp) +) { + Box( + modifier = modifier + ){ + if (url.isNullOrEmpty()) { + Image( + modifier = Modifier.fillMaxSize(), + asset = errorImage + ) + } else { + Surface( + color = Color.Transparent, + shape = shape + ) { + CoilImageWithCrossfade( + data = url, + contentScale = contentScale + ) + } + } + } +} + diff --git a/app/src/main/java/com/akash/newzz_compose/ui/home/BodyContent.kt b/app/src/main/java/com/akash/newzz_compose/ui/home/BodyContent.kt new file mode 100644 index 0000000..0932eea --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/home/BodyContent.kt @@ -0,0 +1,176 @@ +package com.akash.newzz_compose.ui.home + +import androidx.compose.Composable +import androidx.compose.State +import androidx.ui.core.Alignment +import androidx.ui.core.Modifier +import androidx.ui.foundation.Box +import androidx.ui.foundation.Icon +import androidx.ui.foundation.Text +import androidx.ui.foundation.shape.corner.RoundedCornerShape +import androidx.ui.graphics.Color +import androidx.ui.layout.* +import androidx.ui.livedata.observeAsState +import androidx.ui.material.CircularProgressIndicator +import androidx.ui.material.IconButton +import androidx.ui.material.Surface +import androidx.ui.material.TextButton +import androidx.ui.res.vectorResource +import androidx.ui.text.TextStyle +import androidx.ui.unit.dp +import com.akash.newzz_compose.Category +import com.akash.newzz_compose.R +import com.akash.newzz_compose.style.* +import com.akash.newzz_compose.ui.articleListPage.ArticleList +import com.akash.newzz_compose.viewmodel.NewzzViewModel + + +/** + * Created by Akash on 05/06/20 + */ + +@Composable +fun BodyContent(viewModel: NewzzViewModel) { + val page = viewModel.pageNumber.observeAsState(NewzzViewModel.General) + val generalState = viewModel.generalState.observeAsState(NewzzViewModel.ArticleState()) + val businessState = viewModel.businessState.observeAsState(NewzzViewModel.ArticleState()) + val techState = viewModel.techState.observeAsState(NewzzViewModel.ArticleState()) + val isDark = viewModel.isDarkTheme.observeAsState(false) + Surface(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxWidth()) { + TopAppBar(viewModel) + Surface( + color = if (isDark.value) listBackgroundColorDark else listBackgroundColor, + shape = RoundedCornerShape(topLeft = 5.dp, topRight = 5.dp), + modifier = Modifier.fillMaxSize().padding( + start = 10.dp, + end = 10.dp, + bottom = 54.dp + ) + ) { + when (page.value) { + NewzzViewModel.General -> ArticleStateWidget( + state = generalState, + isDark = isDark, + onClick = { + viewModel.performAction(NewzzViewModel.Action.FetchArticles(Category.GENERAL)) + } + ) + NewzzViewModel.Business -> ArticleStateWidget( + state = businessState, + isDark = isDark, + onClick = { + viewModel.performAction(NewzzViewModel.Action.FetchArticles(Category.BUSINESS)) + } + ) + NewzzViewModel.Technology -> ArticleStateWidget( + state = techState, + isDark = isDark, + onClick = { + viewModel.performAction(NewzzViewModel.Action.FetchArticles(Category.TECH)) + } + ) + } + } + } + } +} + +@Composable +fun TopAppBar(viewModel: NewzzViewModel) { + val page = viewModel.pageNumber.observeAsState(NewzzViewModel.General) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalGravity = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.padding(top = 40.dp, start = 16.dp, end = 16.dp, bottom = 16.dp), + text = getTitle(page.value), + style = categoryTitleStyle + ) + Box(modifier = Modifier.padding(top = 24.dp, end = 8.dp)) { + ThemeSwitcher(viewModel = viewModel) + } + } +} + +@Composable +fun ArticleStateWidget(state: State, isDark: State, onClick: () -> Unit) { + when { + state.value.isLoading -> { + Loading(isDark) + } + state.value.list != null -> { + ArticleList(articles = state.value.list!!, isDark = isDark) + } + else -> { + ErrorView( + errorMessage = state.value.error!!.errorMessage, + showRetry = state.value.error!!.showRetry, + onClick = onClick, + isDark = isDark + ) + } + } +} + + +@Composable +fun ThemeSwitcher(viewModel: NewzzViewModel) { + val isDark = viewModel.isDarkTheme.observeAsState(false) + val light = vectorResource(id = R.drawable.ic_light) + val dark = vectorResource(id = R.drawable.ic_dark) + IconButton(onClick = { + viewModel.performAction(NewzzViewModel.Action.SwitchTheme) + }) { + Icon( + asset = if (isDark.value) light else dark, + tint = Color.White + ) + } +} + + +@Composable +fun Loading(isDark: State) { + Column( + verticalArrangement = Arrangement.Center, + horizontalGravity = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + color = if (isDark.value) circularLoaderColorDark else circularLoaderColor + ) + } +} + +@Composable +fun ErrorView(errorMessage: String, showRetry: Boolean, isDark: State, onClick: () -> Unit) { + Column( + verticalArrangement = Arrangement.Center, + horizontalGravity = Alignment.CenterHorizontally + ) { + Text( + text = errorMessage, + style = if(isDark.value) articleTitleStyle.copy(color = titleColorDark) else articleTitleStyle + ) + if (showRetry) { + TextButton(onClick = onClick) { + Text( + text = "Retry", + style = TextStyle( + color = if(isDark.value) sourceTextColorDark else deepPurple + ) + ) + } + } + } +} + + +private fun getTitle(pageNumber: Int): String = when (pageNumber) { + 1 -> "General" + 2 -> "Business" + 3 -> "Technology" + else -> throw IllegalAccessException("Page number is invalid") +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/ui/home/BottomBar.kt b/app/src/main/java/com/akash/newzz_compose/ui/home/BottomBar.kt new file mode 100644 index 0000000..857b34e --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/home/BottomBar.kt @@ -0,0 +1,88 @@ +package com.akash.newzz_compose.ui.home + +import androidx.compose.Composable +import androidx.compose.State +import androidx.ui.core.Modifier +import androidx.ui.foundation.Icon +import androidx.ui.layout.Arrangement +import androidx.ui.layout.Row +import androidx.ui.livedata.observeAsState +import androidx.ui.material.BottomAppBar +import androidx.ui.material.IconButton +import androidx.ui.res.vectorResource +import com.akash.newzz_compose.R +import com.akash.newzz_compose.style.* +import com.akash.newzz_compose.viewmodel.NewzzViewModel + +/** + * Created by Akash on 05/06/20 + */ + +@Composable +fun BottomBar(viewModel: NewzzViewModel) { + val isDark = viewModel.isDarkTheme.observeAsState(false) + BottomAppBar(backgroundColor = if (isDark.value) bottomNavBackgroundDark else bottomNavBackground) { + val page = viewModel.pageNumber.observeAsState(NewzzViewModel.General) + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.SpaceAround + ) { + BottomNavItem( + asset = R.drawable.ic_general, + isSelected = page.value == NewzzViewModel.General, + isDark = isDark, + onClick = { + if (page.value != NewzzViewModel.General) { + viewModel.performAction( + NewzzViewModel.Action.ChangePageTo( + NewzzViewModel.General + ) + ) + } + } + ) + BottomNavItem( + asset = R.drawable.ic_business, + isSelected = page.value == NewzzViewModel.Business, + isDark = isDark, + onClick = { + if (page.value != NewzzViewModel.Business) { + viewModel.performAction( + NewzzViewModel.Action.ChangePageTo( + NewzzViewModel.Business + ) + ) + } + } + ) + BottomNavItem( + asset = R.drawable.ic_tech, + isSelected = page.value == NewzzViewModel.Technology, + isDark = isDark, + onClick = { + if (page.value != NewzzViewModel.Technology) { + viewModel.performAction( + NewzzViewModel.Action.ChangePageTo( + NewzzViewModel.Technology + ) + ) + } + } + ) + } + } +} + +@Composable +fun BottomNavItem(asset: Int, isDark: State, onClick: () -> Unit, isSelected: Boolean = false) { + IconButton(onClick = onClick) { + Icon( + asset = vectorResource(id = asset), + tint = if (isDark.value) { + if (isSelected) bottomNavIconActiveColorDark else bottomNavIconInActiveColorDark + } else { + if (isSelected) bottomNavIconActiveColor else bottomNavIconInActiveColor + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/ui/home/Home.kt b/app/src/main/java/com/akash/newzz_compose/ui/home/Home.kt new file mode 100644 index 0000000..60b32c2 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/ui/home/Home.kt @@ -0,0 +1,19 @@ +package com.akash.newzz_compose.ui.home + +import androidx.compose.Composable +import androidx.ui.material.Scaffold +import com.akash.newzz_compose.viewmodel.NewzzViewModel + +/** + * Created by Akash on 05/06/20 + */ + +@Composable +fun Home( + viewModel: NewzzViewModel +) { + Scaffold( + bodyContent = { BodyContent(viewModel) }, + bottomAppBar = { BottomBar(viewModel) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/utils/CustomTabUtil.kt b/app/src/main/java/com/akash/newzz_compose/utils/CustomTabUtil.kt new file mode 100644 index 0000000..ef58d15 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/utils/CustomTabUtil.kt @@ -0,0 +1,60 @@ +package com.akash.newzz_compose.utils + +import android.content.Context +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat +import com.akash.newzz_compose.R + +/** + * Created by Akash on 07/06/20 + */ +object CustomTabUtil { + private var builder: CustomTabsIntent? = null + private var builderDark: CustomTabsIntent? = null + + fun launch(context: Context, url: String, isDark: Boolean) { + if (isDark) { + if (builderDark == null) { + builderDark = CustomTabsIntent.Builder() + .setToolbarColor( + ContextCompat.getColor(context, R.color.darkTheme) + ) + .setShowTitle(true) + .setStartAnimations( + context, + R.anim.slide_in_right, + R.anim.slide_out_left + ) + .setExitAnimations( + context, + android.R.anim.slide_in_left, + android.R.anim.slide_out_right + ) + .build() + } + builderDark?.launchUrl(context, Uri.parse(url)) + } else { + if (builder == null) { + builder = CustomTabsIntent.Builder() + .setToolbarColor( + ContextCompat.getColor(context, R.color.lightTheme) + ) + .setShowTitle(true) + .setStartAnimations( + context, + R.anim.slide_in_right, + R.anim.slide_out_left + ) + .setExitAnimations( + context, + android.R.anim.slide_in_left, + android.R.anim.slide_out_right + ) + .build() + } + builder?.launchUrl(context, Uri.parse(url)) + } + } +} + diff --git a/app/src/main/java/com/akash/newzz_compose/utils/Result.kt b/app/src/main/java/com/akash/newzz_compose/utils/Result.kt new file mode 100644 index 0000000..c4328a5 --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/utils/Result.kt @@ -0,0 +1,10 @@ +package com.akash.newzz_compose.utils + +/** + * Created by Akash on 06/06/20 + */ + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val errorMessage: String, val showRetry: Boolean = true) : Result() +} \ No newline at end of file diff --git a/app/src/main/java/com/akash/newzz_compose/viewmodel/NewzzViewModel.kt b/app/src/main/java/com/akash/newzz_compose/viewmodel/NewzzViewModel.kt new file mode 100644 index 0000000..83f61ac --- /dev/null +++ b/app/src/main/java/com/akash/newzz_compose/viewmodel/NewzzViewModel.kt @@ -0,0 +1,207 @@ +package com.akash.newzz_compose.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.akash.newzz_compose.Category +import com.akash.newzz_compose.data.repository.NewsRepository +import com.akash.newzz_compose.data.response.NewsArticle +import com.akash.newzz_compose.utils.Result +import kotlinx.coroutines.* + +/** + * Created by Akash on 06/06/20 + */ + +class NewzzViewModel(private val repo: NewsRepository) : ViewModel() { + + private val job = SupervisorJob() + private val viewModelScope = CoroutineScope(Dispatchers.Main) + job + + private val _pageNumber = MutableLiveData().apply { + value = + General + } + val pageNumber: LiveData = _pageNumber + + private val _isDarkTheme = MutableLiveData().apply { value = false } + val isDarkTheme: LiveData = _isDarkTheme + + private val generalArticlesListState = ArticleState() + private val businessArticlesListState = ArticleState() + private val techArticlesListState = ArticleState() + + private val _generalState = MutableLiveData().apply { + value = generalArticlesListState + } + val generalState: LiveData = _generalState + + private val _businessState = MutableLiveData().apply { + value = businessArticlesListState + } + val businessState: LiveData = _businessState + + private val _techState = MutableLiveData().apply { + value = techArticlesListState + } + val techState: LiveData = _techState + + init { + performAction(Action.FetchArticles(Category.GENERAL)) + } + + fun performAction(action: Action) { + when (action) { + is Action.ChangePageTo -> { + _pageNumber.value = action.page + when (action.page) { + General -> performAction( + Action.FetchArticles(Category.GENERAL) + ) + Business -> performAction( + Action.FetchArticles(Category.BUSINESS) + ) + Technology -> performAction( + Action.FetchArticles(Category.TECH) + ) + } + } + is Action.FetchArticles -> viewModelScope.launch { + withContext(Dispatchers.IO) { + fetArticles(action.category) + } + } + is Action.SwitchTheme -> { + _isDarkTheme.value = !_isDarkTheme.value!! + } + } + } + + private suspend fun fetArticles(category: String) { + when (category) { + Category.GENERAL -> fetchGeneralArticles(category) + Category.BUSINESS -> fetchBusinessArticles(category) + Category.TECH -> fetchTechArticles(category) + } + } + + private suspend fun fetchGeneralArticles(category: String) { + val state = generalState.value!! + if (state.list == null || state.list.isEmpty()) { + withContext(Dispatchers.Main) { + val articleState = ArticleState() + _generalState.value = articleState + } + when (val result = repo.getArticlesByCategoryAsync(category, 1)) { + is Result.Success -> { + withContext(Dispatchers.Main) { + val articleState = generalArticlesListState.copy( + isLoading = false, + list = result.data.articles, + error = null + ) + _generalState.value = articleState + } + } + is Result.Error -> { + withContext(Dispatchers.Main) { + val articleState = generalArticlesListState.copy( + isLoading = false, + list = null, + error = Error(result.errorMessage, result.showRetry) + ) + _generalState.value = articleState + } + } + } + } + } + + private suspend fun fetchBusinessArticles(category: String) { + val state = businessState.value!! + if (state.list == null || state.list.isEmpty()) { + withContext(Dispatchers.Main){ + val articleState = ArticleState() + _businessState.value = articleState + } + when (val result = repo.getArticlesByCategoryAsync(category, 1)) { + is Result.Success -> { + withContext(Dispatchers.Main) { + val articleState = businessArticlesListState.copy( + isLoading = false, + list = result.data.articles, + error = null + ) + _businessState.value = articleState + } + } + is Result.Error -> { + withContext(Dispatchers.Main) { + val articleState = businessArticlesListState.copy( + isLoading = false, + list = null, + error = Error(result.errorMessage, result.showRetry) + ) + _businessState.value = articleState + } + } + } + } + } + + private suspend fun fetchTechArticles(category: String) { + val state = techState.value!! + if (state.list == null || state.list.isEmpty()) { + withContext(Dispatchers.Main){ + val articleState = ArticleState() + _techState.value = articleState + } + when (val result = repo.getArticlesByCategoryAsync(category, 1)) { + is Result.Success -> { + withContext(Dispatchers.Main) { + val articleState = techArticlesListState.copy( + isLoading = false, + list = result.data.articles, + error = null + ) + _techState.value = articleState + } + } + is Result.Error -> { + withContext(Dispatchers.Main) { + val articleState = techArticlesListState.copy( + isLoading = false, + list = null, + error = Error(result.errorMessage, result.showRetry) + ) + _techState.value = articleState + } + } + } + } + } + + sealed class Action { + data class ChangePageTo(val page: Int) : Action() + data class FetchArticles(val category: String) : Action() + object SwitchTheme : Action() + } + + data class ArticleState( + val isLoading: Boolean = true, + val list: List? = emptyList(), + val error: Error? = null + ) + + data class Error( + val errorMessage : String, + val showRetry : Boolean = true + ) + + companion object { + const val General = 1 + const val Business = 2 + const val Technology = 3 + } + +} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..8f18d59 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..0a5f875 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_business.xml b/app/src/main/res/drawable/ic_business.xml new file mode 100644 index 0000000..26d1a50 --- /dev/null +++ b/app/src/main/res/drawable/ic_business.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dark.xml b/app/src/main/res/drawable/ic_dark.xml new file mode 100644 index 0000000..c097af7 --- /dev/null +++ b/app/src/main/res/drawable/ic_dark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_general.xml b/app/src/main/res/drawable/ic_general.xml new file mode 100644 index 0000000..afbda8d --- /dev/null +++ b/app/src/main/res/drawable/ic_general.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_light.xml b/app/src/main/res/drawable/ic_light.xml new file mode 100644 index 0000000..f674100 --- /dev/null +++ b/app/src/main/res/drawable/ic_light.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_newzz_error.xml b/app/src/main/res/drawable/ic_newzz_error.xml new file mode 100644 index 0000000..945e792 --- /dev/null +++ b/app/src/main/res/drawable/ic_newzz_error.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_tech.xml b/app/src/main/res/drawable/ic_tech.xml new file mode 100644 index 0000000..ffb5131 --- /dev/null +++ b/app/src/main/res/drawable/ic_tech.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..d0bfae3 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #6200EE + #3700B3 + #03DAC5 + #FF121212 + #FF4a148c + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5577ecb --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Newzz-Compose + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..f5ff934 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + +