sirekanian 3 years ago
commit
7d2124a5ac
68 changed files with 1818 additions and 0 deletions
  1. 10 0
      .gitignore
  2. 1 0
      app/.gitignore
  3. 65 0
      app/build.gradle
  4. 21 0
      app/proguard.pro
  5. 30 0
      app/src/main/AndroidManifest.xml
  6. BIN
      app/src/main/ic_launcher2-playstore.png
  7. 12 0
      app/src/main/java/com/sirekanian/acf/App.kt
  8. 18 0
      app/src/main/java/com/sirekanian/acf/D.kt
  9. 107 0
      app/src/main/java/com/sirekanian/acf/MainActivity.kt
  10. 45 0
      app/src/main/java/com/sirekanian/acf/MainPresenter.kt
  11. 72 0
      app/src/main/java/com/sirekanian/acf/MainState.kt
  12. 69 0
      app/src/main/java/com/sirekanian/acf/data/Repository.kt
  13. 37 0
      app/src/main/java/com/sirekanian/acf/data/Warmonger.kt
  14. 11 0
      app/src/main/java/com/sirekanian/acf/data/local/Database.kt
  15. 35 0
      app/src/main/java/com/sirekanian/acf/data/local/WarmongerDao.kt
  16. 13 0
      app/src/main/java/com/sirekanian/acf/data/local/WarmongerEntity.kt
  17. 23 0
      app/src/main/java/com/sirekanian/acf/data/remote/Network.kt
  18. 13 0
      app/src/main/java/com/sirekanian/acf/data/remote/WarmongerDto.kt
  19. 6 0
      app/src/main/java/com/sirekanian/acf/ext/Activity.kt
  20. 12 0
      app/src/main/java/com/sirekanian/acf/ext/AnimatedVisibility.kt
  21. 16 0
      app/src/main/java/com/sirekanian/acf/ext/Modifier.kt
  22. 4 0
      app/src/main/java/com/sirekanian/acf/ext/Number.kt
  23. 18 0
      app/src/main/java/com/sirekanian/acf/ext/PaddingValues.kt
  24. 9 0
      app/src/main/java/com/sirekanian/acf/ext/Resources.kt
  25. 33 0
      app/src/main/java/com/sirekanian/acf/ui/MainContent.kt
  26. 19 0
      app/src/main/java/com/sirekanian/acf/ui/MainFab.kt
  27. 36 0
      app/src/main/java/com/sirekanian/acf/ui/MainProgress.kt
  28. 105 0
      app/src/main/java/com/sirekanian/acf/ui/MainSearch.kt
  29. 54 0
      app/src/main/java/com/sirekanian/acf/ui/MainToolbar.kt
  30. 69 0
      app/src/main/java/com/sirekanian/acf/ui/WarmongerCard.kt
  31. 62 0
      app/src/main/java/com/sirekanian/acf/ui/theme/Color3.kt
  32. 12 0
      app/src/main/java/com/sirekanian/acf/ui/theme/Shape.kt
  33. 75 0
      app/src/main/java/com/sirekanian/acf/ui/theme/Theme.kt
  34. 30 0
      app/src/main/java/com/sirekanian/acf/ui/theme/Type.kt
  35. 100 0
      app/src/main/java/com/sirekanian/acf/ui/theme/Type3.kt
  36. 74 0
      app/src/main/res/drawable/ic_launcher2_background.xml
  37. 26 0
      app/src/main/res/drawable/ic_launcher2_foreground.xml
  38. BIN
      app/src/main/res/font/montserrat_light.ttf
  39. BIN
      app/src/main/res/font/montserrat_medium.ttf
  40. BIN
      app/src/main/res/font/montserrat_regular.ttf
  41. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher2.xml
  42. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher2_round.xml
  43. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher2.png
  44. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher2_round.png
  45. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher2.png
  46. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher2_round.png
  47. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher2.png
  48. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png
  49. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher2.png
  50. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png
  51. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png
  52. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png
  53. 82 0
      app/src/main/res/raw/default1.json
  54. 7 0
      app/src/main/res/values-be/strings.xml
  55. 11 0
      app/src/main/res/values-notnight/themes.xml
  56. 7 0
      app/src/main/res/values-ru/strings.xml
  57. 7 0
      app/src/main/res/values-uk/strings.xml
  58. 3 0
      app/src/main/res/values/colors.xml
  59. 8 0
      app/src/main/res/values/strings.xml
  60. 11 0
      app/src/main/res/values/themes.xml
  61. 20 0
      build.gradle
  62. 4 0
      gradle.properties
  63. BIN
      gradle/wrapper/gradle-wrapper.jar
  64. 6 0
      gradle/wrapper/gradle-wrapper.properties
  65. 185 0
      gradlew
  66. 89 0
      gradlew.bat
  67. 19 0
      settings.gradle
  68. 7 0
      upd.sh

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 65 - 0
app/build.gradle

@@ -0,0 +1,65 @@
+plugins {
+    id 'com.android.application'
+    id 'org.jetbrains.kotlin.android'
+    id 'org.jetbrains.kotlin.plugin.serialization'
+    id 'org.jetbrains.kotlin.kapt'
+}
+
+android {
+    compileSdk 32
+    defaultConfig {
+        applicationId "com.sirekanian.warmongr"
+        minSdk 21
+        targetSdk 32
+        versionCode 0
+        versionName "0.0.0"
+        archivesBaseName = "$applicationId-$versionName-$versionCode"
+        vectorDrawables {
+            useSupportLibrary true
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+    buildFeatures {
+        compose true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion composeVersion
+    }
+    packagingOptions {
+        resources {
+            excludes += '/META-INF/{AL2.0,LGPL2.1}'
+        }
+    }
+}
+
+dependencies {
+
+    // androidx compose
+    implementation "androidx.compose.material:material:$composeVersion"
+    implementation 'androidx.activity:activity-compose:1.4.0'
+
+    // androidx room
+    implementation "androidx.room:room-runtime:$roomVersion"
+    implementation "androidx.room:room-ktx:$roomVersion"
+    kapt "androidx.room:room-compiler:$roomVersion"
+
+    // ktor
+    implementation "io.ktor:ktor-client-cio:$ktorVersion"
+    implementation "io.ktor:ktor-client-content-negotiation:$ktorVersion"
+    implementation "io.ktor:ktor-client-encoding:$ktorVersion"
+    implementation "io.ktor:ktor-serialization-kotlinx-json:$ktorVersion"
+
+}

+ 21 - 0
app/proguard.pro

@@ -0,0 +1,21 @@
+# Kotlinx Serialization
+# https://github.com/Kotlin/kotlinx.serialization#android
+
+-if @kotlinx.serialization.Serializable class **
+-keepclassmembers class <1> {
+    static <1>$Companion Companion;
+}
+-if @kotlinx.serialization.Serializable class ** {
+    static **$* *;
+}
+-keepclassmembers class <2>$<3> {
+    kotlinx.serialization.KSerializer serializer(...);
+}
+-if @kotlinx.serialization.Serializable class ** {
+    public static ** INSTANCE;
+}
+-keepclassmembers class <1> {
+    public static <1> INSTANCE;
+    kotlinx.serialization.KSerializer serializer(...);
+}
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

+ 30 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.sirekanian.acf">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application
+        android:name=".App"
+        android:allowBackup="false"
+        android:icon="@mipmap/ic_launcher2"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher2_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.Warmongr">
+
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:theme="@style/Theme.Warmongr">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+        </activity>
+
+    </application>
+
+</manifest>

BIN
app/src/main/ic_launcher2-playstore.png


+ 12 - 0
app/src/main/java/com/sirekanian/acf/App.kt

@@ -0,0 +1,12 @@
+package com.sirekanian.acf
+
+import android.app.Application
+import androidx.room.Room
+import com.sirekanian.acf.data.Repository
+import com.sirekanian.acf.data.RepositoryImpl
+import com.sirekanian.acf.data.local.Database
+
+class App : Application() {
+    private val db by lazy { Room.databaseBuilder(this, Database::class.java, "database").build() }
+    val repository: Repository by lazy { RepositoryImpl(resources, db.warmongerDao()) }
+}

+ 18 - 0
app/src/main/java/com/sirekanian/acf/D.kt

@@ -0,0 +1,18 @@
+package com.sirekanian.acf
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.unit.dp
+
+object D {
+    val listPaddings = PaddingValues(16.dp, 8.dp, 16.dp, 16.dp)
+    val toolbarSize = 64.dp
+    val toolbarElevation = 8.dp
+    val fabSize = 96.dp
+    val fabPadding = 16.dp
+    val cornerSizeSmall = 12.dp
+    val cornerSizeMedium = 28.dp
+    val cardDefaultElevation = 2.dp
+    val cardSelectedElevation = 4.dp
+    val cardSpacing = 8.dp
+    val progressSize = 2.dp
+}

+ 107 - 0
app/src/main/java/com/sirekanian/acf/MainActivity.kt

@@ -0,0 +1,107 @@
+package com.sirekanian.acf
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.core.view.WindowCompat
+import com.sirekanian.acf.ext.app
+import com.sirekanian.acf.ui.MainContent
+import com.sirekanian.acf.ui.MainFab
+import com.sirekanian.acf.ui.MainProgress
+import com.sirekanian.acf.ui.MainToolbar
+import com.sirekanian.acf.ui.theme.WarmongrTheme
+
+class MainActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+        setContent {
+            val state = remember { MainState() }
+            val presenter = remember { createPresenter(app(), state) }
+            val data by presenter.observeData().collectAsState(listOf())
+            val hasData by derivedStateOf { data.isNotEmpty() }
+            LaunchedEffect(Unit) {
+                try {
+                    presenter.initData()
+                } catch (exception: Exception) {
+                    Log.e("Warmongr", "Cannot initialize data", exception)
+                }
+            }
+            BackHandler(enabled = state.search.isOpened) {
+                state.search.isOpened = false
+            }
+            WarmongrTheme {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .background(MaterialTheme.colors.background)
+                ) {
+                    MainLayout(
+                        toolbar = { insets ->
+                            MainToolbar(insets, state.search)
+                            MainProgress(insets, state.progress)
+                        },
+                        toolbarElevation = state.toolbarElevation,
+                        content = { insets -> MainContent(insets, state.list, data) },
+                        contentVisible = hasData,
+                        fab = { MainFab(onClick = { state.search.isOpened = true }) },
+                        fabVisible = !state.search.isOpened,
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun MainLayout(
+    toolbar: @Composable (PaddingValues) -> Unit,
+    toolbarElevation: Dp,
+    content: @Composable (PaddingValues) -> Unit,
+    contentVisible: Boolean,
+    fab: @Composable () -> Unit,
+    fabVisible: Boolean,
+) {
+    AnimatedVisibility(contentVisible, enter = fadeIn(), exit = fadeOut()) {
+        content(WindowInsets.systemBars.asPaddingValues())
+    }
+    Surface(Modifier.fillMaxWidth(), elevation = toolbarElevation) {
+        toolbar(WindowInsets.statusBars.asPaddingValues())
+    }
+    AnimatedVisibility(fabVisible, enter = fadeIn(), exit = fadeOut()) {
+        BottomBox(Modifier.padding(D.fabPadding)) {
+            fab()
+        }
+    }
+    if (MaterialTheme.colors.isLight) {
+        BottomBox {
+            Divider()
+        }
+    }
+}
+
+@Composable
+fun BottomBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Box(
+        modifier
+            .navigationBarsPadding()
+            .fillMaxSize(),
+        contentAlignment = Alignment.BottomCenter
+    ) {
+        content()
+    }
+}

+ 45 - 0
app/src/main/java/com/sirekanian/acf/MainPresenter.kt

@@ -0,0 +1,45 @@
+package com.sirekanian.acf
+
+import com.sirekanian.acf.data.Repository
+import com.sirekanian.acf.data.Warmonger
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+private val UPDATE_PERIOD = if (BuildConfig.DEBUG) 20.seconds else 5.minutes
+
+fun createPresenter(app: App, state: MainState): MainPresenter =
+    MainPresenterImpl(app.repository, state)
+
+interface MainPresenter {
+    fun observeData(): Flow<List<Warmonger>>
+    suspend fun initData()
+}
+
+class MainPresenterImpl(
+    private val repository: Repository,
+    private val state: MainState,
+) : MainPresenter {
+
+    override fun observeData(): Flow<List<Warmonger>> =
+        if (state.search.isOpened) {
+            repository.observeByQuery(state.search.query.text)
+        } else {
+            repository.observeAll()
+        }
+
+    override suspend fun initData() =
+        withContext(IO) {
+            if (!repository.hasData()) {
+                repository.initLocal()
+            }
+            while (true) {
+                repository.updateFromRemote(state.progress)
+                delay(UPDATE_PERIOD)
+            }
+        }
+
+}

+ 72 - 0
app/src/main/java/com/sirekanian/acf/MainState.kt

@@ -0,0 +1,72 @@
+package com.sirekanian.acf
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+
+class MainState {
+    val list = ListState()
+    val search = SearchState()
+    val progress = ProgressState()
+    val toolbarElevation by derivedStateOf {
+        if (search.isOpened || !list.isTopVisible) {
+            D.toolbarElevation
+        } else {
+            0.dp
+        }
+    }
+}
+
+class ListState {
+    val lazyListState = LazyListState()
+    val isTopVisible by derivedStateOf {
+        lazyListState.firstVisibleItemIndex == 0 &&
+                lazyListState.firstVisibleItemScrollOffset == 0
+    }
+}
+
+class SearchState {
+    var query by mutableStateOf(TextFieldValue())
+    var isOpened by mutableStateOf(false)
+}
+
+class ProgressState {
+
+    private var current by mutableStateOf<Long?>(null)
+    private var total by mutableStateOf<Long?>(null)
+
+    fun isVisible(): Boolean =
+        current != null && total != null
+
+    fun show() {
+        current = 0
+        total = 1
+    }
+
+    fun hide() {
+        current = null
+        total = null
+    }
+
+    fun setIndeterminate() {
+        current = 0
+        total = 0
+    }
+
+    fun setDeterminate(current: Long, total: Long) {
+        this.current = current
+        this.total = total
+    }
+
+    fun calculateProgressOrNull(): Float? {
+        val current = current ?: return null
+        val total = total ?: return null
+        if (total == 0L) return null
+        return (current.toFloat() / total).coerceIn(0f, 1f)
+    }
+
+}

+ 69 - 0
app/src/main/java/com/sirekanian/acf/data/Repository.kt

@@ -0,0 +1,69 @@
+package com.sirekanian.acf.data
+
+import android.content.res.Resources
+import android.database.sqlite.SQLiteException
+import com.sirekanian.acf.ProgressState
+import com.sirekanian.acf.R
+import com.sirekanian.acf.data.local.WarmongerDao
+import com.sirekanian.acf.data.local.WarmongerEntity
+import com.sirekanian.acf.data.remote.WarmongerDto
+import com.sirekanian.acf.data.remote.getWarmongers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+
+interface Repository {
+    fun observeAll(): Flow<List<Warmonger>>
+    fun observeByQuery(query: String): Flow<List<Warmonger>>
+    suspend fun hasData(): Boolean
+    suspend fun initLocal()
+    suspend fun updateFromRemote(progressState: ProgressState)
+}
+
+class RepositoryImpl(
+    private val resources: Resources,
+    private val dao: WarmongerDao,
+) : Repository {
+
+    override fun observeAll(): Flow<List<Warmonger>> =
+        dao.observeAll().fromEntities()
+
+    override fun observeByQuery(query: String): Flow<List<Warmonger>> =
+        dao.observeByQuery("$query*").fromEntities()
+
+    private fun Flow<List<WarmongerEntity>>.fromEntities(): Flow<List<Warmonger>> =
+        map { it.map(Warmonger::fromEntity) }.catch {
+            if (it is SQLiteException) {
+                emit(listOf())
+            } else {
+                throw it
+            }
+        }
+
+    override suspend fun hasData(): Boolean =
+        dao.hasData()
+
+    override suspend fun initLocal() {
+        val content = resources.openRawResource(R.raw.default1).bufferedReader().readText()
+        val local = Json.decodeFromString<List<WarmongerDto>>(content)
+        saveToLocal(local.map(Warmonger::fromDto))
+    }
+
+    override suspend fun updateFromRemote(progressState: ProgressState) {
+        try {
+            progressState.show()
+            val remote = getWarmongers(progressState::setDeterminate).map(Warmonger::fromDto)
+            progressState.setIndeterminate()
+            saveToLocal(remote)
+        } finally {
+            progressState.hide()
+        }
+    }
+
+    private suspend fun saveToLocal(users: List<Warmonger>) {
+        dao.deleteAndInsertAll(users.map(Warmonger::toEntity))
+    }
+
+}

+ 37 - 0
app/src/main/java/com/sirekanian/acf/data/Warmonger.kt

@@ -0,0 +1,37 @@
+package com.sirekanian.acf.data
+
+import com.sirekanian.acf.data.local.WarmongerEntity
+import com.sirekanian.acf.data.remote.WarmongerDto
+
+class Warmonger(
+    val cyrillicName: String,
+    val name: String,
+    val notes: String,
+) {
+
+    companion object {
+
+        fun fromDto(dto: WarmongerDto): Warmonger =
+            Warmonger(
+                cyrillicName = dto.`0`,
+                name = dto.`1`.ifEmpty { dto.`0` },
+                notes = dto.`4`,
+            )
+
+        fun fromEntity(entity: WarmongerEntity): Warmonger =
+            Warmonger(
+                cyrillicName = entity.cyrillicName,
+                name = entity.name,
+                notes = entity.notes,
+            )
+
+        fun toEntity(model: Warmonger): WarmongerEntity =
+            WarmongerEntity(
+                cyrillicName = model.cyrillicName,
+                name = model.name,
+                notes = model.notes,
+            )
+
+    }
+
+}

+ 11 - 0
app/src/main/java/com/sirekanian/acf/data/local/Database.kt

@@ -0,0 +1,11 @@
+package com.sirekanian.acf.data.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(entities = [WarmongerEntity::class], version = 1)
+abstract class Database : RoomDatabase() {
+
+    abstract fun warmongerDao(): WarmongerDao
+
+}

+ 35 - 0
app/src/main/java/com/sirekanian/acf/data/local/WarmongerDao.kt

@@ -0,0 +1,35 @@
+package com.sirekanian.acf.data.local
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+private const val LIMIT = 200
+
+@Dao
+interface WarmongerDao {
+
+    @Query("SELECT * FROM WarmongerEntity ORDER BY cyrillicName LIMIT $LIMIT")
+    fun observeAll(): Flow<List<WarmongerEntity>>
+
+    @Query("SELECT * FROM WarmongerEntity WHERE WarmongerEntity MATCH :query LIMIT $LIMIT")
+    fun observeByQuery(query: String): Flow<List<WarmongerEntity>>
+
+    @Query("SELECT count(1) FROM WarmongerEntity LIMIT 1")
+    suspend fun hasData(): Boolean
+
+    @Query("DELETE FROM WarmongerEntity")
+    suspend fun deleteAll()
+
+    @Insert
+    suspend fun insertAll(users: List<WarmongerEntity>)
+
+    @Transaction
+    suspend fun deleteAndInsertAll(users: List<WarmongerEntity>) {
+        deleteAll()
+        insertAll(users)
+    }
+
+}

+ 13 - 0
app/src/main/java/com/sirekanian/acf/data/local/WarmongerEntity.kt

@@ -0,0 +1,13 @@
+package com.sirekanian.acf.data.local
+
+import androidx.room.Entity
+import androidx.room.Fts4
+import androidx.room.FtsOptions
+
+@Fts4(tokenizer = FtsOptions.TOKENIZER_UNICODE61)
+@Entity
+class WarmongerEntity(
+    val cyrillicName: String,
+    val name: String,
+    val notes: String,
+)

+ 23 - 0
app/src/main/java/com/sirekanian/acf/data/remote/Network.kt

@@ -0,0 +1,23 @@
+package com.sirekanian.acf.data.remote
+
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.content.*
+import io.ktor.client.plugins.*
+import io.ktor.client.plugins.compression.*
+import io.ktor.client.plugins.contentnegotiation.*
+import io.ktor.client.request.*
+import io.ktor.serialization.kotlinx.json.*
+
+private const val ACF_URL = "https://sirekanian.github.io/warmongers.json"
+private val httpClient = HttpClient {
+    install(ContentNegotiation) {
+        json()
+    }
+    install(ContentEncoding) {
+        gzip()
+    }
+}
+
+suspend fun getWarmongers(listener: ProgressListener): List<WarmongerDto> =
+    httpClient.get(ACF_URL) { onDownload(listener) }.body()

+ 13 - 0
app/src/main/java/com/sirekanian/acf/data/remote/WarmongerDto.kt

@@ -0,0 +1,13 @@
+package com.sirekanian.acf.data.remote
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class WarmongerDto(
+    val `0`: String,
+    val `1`: String,
+    val `2`: String,
+    val `3`: String,
+    val `4`: String,
+    val `5`: String,
+)

+ 6 - 0
app/src/main/java/com/sirekanian/acf/ext/Activity.kt

@@ -0,0 +1,6 @@
+package com.sirekanian.acf.ext
+
+import android.app.Activity
+import com.sirekanian.acf.App
+
+fun Activity.app() = application as App

+ 12 - 0
app/src/main/java/com/sirekanian/acf/ext/AnimatedVisibility.kt

@@ -0,0 +1,12 @@
+package com.sirekanian.acf.ext
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.runtime.Composable
+
+@Composable
+fun DefaultAnimatedVisibility(visible: Boolean, content: @Composable () -> Unit) =
+    AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
+        content()
+    }

+ 16 - 0
app/src/main/java/com/sirekanian/acf/ext/Modifier.kt

@@ -0,0 +1,16 @@
+package com.sirekanian.acf.ext
+
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+
+fun Modifier.pointerInputOnDown(key: Any?, onDownEvent: () -> Unit): Modifier =
+    pointerInput(key) {
+        forEachGesture {
+            awaitPointerEventScope {
+                awaitFirstDown(requireUnconsumed = false)
+                onDownEvent()
+            }
+        }
+    }

+ 4 - 0
app/src/main/java/com/sirekanian/acf/ext/Number.kt

@@ -0,0 +1,4 @@
+package com.sirekanian.acf.ext
+
+fun calculateProgressOrNull(current: Long, total: Long): Float? =
+    if (total == 0L) null else (current.toFloat() / total).coerceIn(0f, 1f)

+ 18 - 0
app/src/main/java/com/sirekanian/acf/ext/PaddingValues.kt

@@ -0,0 +1,18 @@
+package com.sirekanian.acf.ext
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalLayoutDirection
+
+@Composable
+operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
+    val direction = LocalLayoutDirection.current
+    return PaddingValues(
+        start = calculateStartPadding(direction) + other.calculateStartPadding(direction),
+        top = calculateTopPadding() + other.calculateTopPadding(),
+        end = calculateEndPadding(direction) + other.calculateEndPadding(direction),
+        bottom = calculateBottomPadding() + other.calculateBottomPadding(),
+    )
+}

+ 9 - 0
app/src/main/java/com/sirekanian/acf/ext/Resources.kt

@@ -0,0 +1,9 @@
+package com.sirekanian.acf.ext
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.booleanResource
+import com.sirekanian.acf.R
+
+@Composable
+fun isCyrillicResources(): Boolean =
+    booleanResource(R.bool.app_cyrillic)

+ 33 - 0
app/src/main/java/com/sirekanian/acf/ui/MainContent.kt

@@ -0,0 +1,33 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import com.sirekanian.acf.D
+import com.sirekanian.acf.ListState
+import com.sirekanian.acf.data.Warmonger
+import com.sirekanian.acf.ext.plus
+import com.sirekanian.acf.ext.pointerInputOnDown
+
+@Composable
+fun MainContent(insets: PaddingValues, listState: ListState, data: List<Warmonger>) {
+    val paddings = PaddingValues(top = D.toolbarSize, bottom = D.fabSize + D.fabPadding)
+    val focusManager = LocalFocusManager.current
+    LazyColumn(
+        modifier = Modifier
+            .fillMaxSize()
+            .pointerInputOnDown(Unit) { focusManager.clearFocus() },
+        state = listState.lazyListState,
+        contentPadding = insets + paddings + D.listPaddings,
+        verticalArrangement = Arrangement.spacedBy(D.cardSpacing)
+    ) {
+        items(data) { item ->
+            WarmongerCard(item)
+        }
+    }
+}

+ 19 - 0
app/src/main/java/com/sirekanian/acf/ui/MainFab.kt

@@ -0,0 +1,19 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.sirekanian.acf.D
+import com.sirekanian.acf.R
+
+@Composable
+fun MainFab(onClick: () -> Unit) {
+    FloatingActionButton(modifier = Modifier.size(D.fabSize), onClick = onClick) {
+        Icon(Icons.Default.Search, stringResource(R.string.app_search_hint))
+    }
+}

+ 36 - 0
app/src/main/java/com/sirekanian/acf/ui/MainProgress.kt

@@ -0,0 +1,36 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.LinearProgressIndicator
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import com.sirekanian.acf.D
+import com.sirekanian.acf.ProgressState
+
+@Composable
+fun MainProgress(insets: PaddingValues, progressState: ProgressState) {
+    AnimatedVisibility(visible = progressState.isVisible(), enter = fadeIn(), exit = fadeOut()) {
+        val progress = progressState.calculateProgressOrNull()
+        val modifier = Modifier
+            .padding(insets)
+            .padding(top = D.toolbarSize - D.progressSize)
+            .fillMaxWidth()
+            .height(D.progressSize)
+            .alpha(ContentAlpha.disabled)
+        val color = MaterialTheme.colors.secondary
+        if (progress == null) {
+            LinearProgressIndicator(modifier, color)
+        } else {
+            LinearProgressIndicator(progress, modifier, color)
+        }
+    }
+}

+ 105 - 0
app/src/main/java/com/sirekanian/acf/ui/MainSearch.kt

@@ -0,0 +1,105 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import com.sirekanian.acf.R
+import com.sirekanian.acf.SearchState
+import com.sirekanian.acf.ext.DefaultAnimatedVisibility
+
+@Composable
+fun MainSearch(searchState: SearchState) {
+    val focusManager = LocalFocusManager.current
+    val focusRequester = remember { FocusRequester() }
+    Row(verticalAlignment = Alignment.CenterVertically) {
+        ToolbarButton(
+            icon = Icons.Default.ArrowBack,
+            onClick = {
+                focusManager.clearFocus()
+                searchState.isOpened = false
+            }
+        )
+        SearchTextField(
+            modifier = Modifier
+                .weight(1f)
+                .padding(horizontal = 8.dp)
+                .focusRequester(focusRequester),
+            searchState = searchState,
+            clearFocus = { focusManager.clearFocus() },
+            requestFocus = { focusRequester.requestFocus() }
+        )
+        ToolbarButton(
+            icon = Icons.Default.Close,
+            visible = searchState.query.text.isNotEmpty(),
+            onClick = {
+                searchState.query = TextFieldValue()
+                focusRequester.requestFocus()
+            }
+        )
+    }
+}
+
+@Composable
+private fun SearchTextField(
+    modifier: Modifier,
+    searchState: SearchState,
+    clearFocus: () -> Unit,
+    requestFocus: () -> Unit,
+) {
+    BasicTextField(
+        value = searchState.query,
+        onValueChange = { searchState.query = it },
+        modifier = modifier,
+        textStyle = MaterialTheme.typography.h6.copy(color = LocalContentColor.current),
+        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+        keyboardActions = KeyboardActions(onSearch = { clearFocus() }),
+        singleLine = true,
+        cursorBrush = SolidColor(MaterialTheme.colors.primary),
+        decorationBox = { innerTextField ->
+            Box {
+                if (searchState.query.text.isEmpty()) {
+                    Text(
+                        text = stringResource(R.string.app_search_hint),
+                        modifier = Modifier.alpha(ContentAlpha.disabled),
+                        style = MaterialTheme.typography.h6.copy(color = LocalContentColor.current)
+                    )
+                }
+                innerTextField()
+            }
+        }
+    )
+    LaunchedEffect(Unit) {
+        requestFocus()
+    }
+}
+
+@Composable
+private fun ToolbarButton(icon: ImageVector, visible: Boolean = true, onClick: () -> Unit) {
+    DefaultAnimatedVisibility(visible = visible) {
+        IconButton(onClick = onClick) {
+            Icon(icon, null)
+        }
+    }
+}

+ 54 - 0
app/src/main/java/com/sirekanian/acf/ui/MainToolbar.kt

@@ -0,0 +1,54 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.sirekanian.acf.D
+import com.sirekanian.acf.R
+import com.sirekanian.acf.SearchState
+
+@Composable
+fun MainToolbar(insets: PaddingValues, searchState: SearchState) {
+    Box(
+        modifier = Modifier
+            .padding(insets)
+            .height(D.toolbarSize),
+        contentAlignment = Alignment.Center,
+    ) {
+        if (searchState.isOpened) {
+            MainSearch(searchState)
+        } else {
+            ToolbarText()
+        }
+    }
+}
+
+@Composable
+private fun ToolbarText() {
+    val text = buildAnnotatedString {
+        append(stringResource(R.string.app_name_part_1))
+        withStyle(SpanStyle(MaterialTheme.colors.primary)) {
+            append(stringResource(R.string.app_name_part_2))
+        }
+        append(stringResource(R.string.app_name_part_3))
+    }
+    Text(
+        text = text,
+        modifier = Modifier.padding(horizontal = 16.dp),
+        style = MaterialTheme.typography.h6,
+        maxLines = 1,
+        overflow = TextOverflow.Ellipsis,
+    )
+}

+ 69 - 0
app/src/main/java/com/sirekanian/acf/ui/WarmongerCard.kt

@@ -0,0 +1,69 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.sirekanian.acf.D
+import com.sirekanian.acf.data.Warmonger
+import com.sirekanian.acf.ext.isCyrillicResources
+
+@Composable
+fun WarmongerCard(warmonger: Warmonger) {
+    var isExpanded by remember { mutableStateOf(false) }
+    val surfaceCornerSize by animateDpAsState(
+        if (isExpanded) {
+            D.cornerSizeSmall
+        } else {
+            D.cornerSizeMedium
+        }
+    )
+    val surfaceElevation by animateDpAsState(
+        if (isExpanded) {
+            D.cardSelectedElevation
+        } else {
+            D.cardDefaultElevation
+        }
+    )
+    Surface(
+        modifier = Modifier.fillMaxWidth(),
+        shape = RoundedCornerShape(surfaceCornerSize),
+        elevation = surfaceElevation
+    ) {
+        WarmongerCardContent(warmonger, isExpanded) { isExpanded = !isExpanded }
+    }
+}
+
+@Composable
+private fun WarmongerCardContent(warmonger: Warmonger, isExpanded: Boolean, onClick: () -> Unit) {
+    Column(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .padding(24.dp)
+            .padding(PaddingValues())
+            .animateContentSize()
+    ) {
+        Text(
+            text = if (isCyrillicResources()) warmonger.cyrillicName else warmonger.name,
+            style = MaterialTheme.typography.h6
+        )
+        Spacer(Modifier.size(12.dp))
+        Text(
+            text = warmonger.notes,
+            modifier = Modifier.alpha(ContentAlpha.medium),
+            style = MaterialTheme.typography.body1,
+            maxLines = if (isExpanded) Int.MAX_VALUE else 2,
+            overflow = TextOverflow.Ellipsis
+        )
+    }
+}

+ 62 - 0
app/src/main/java/com/sirekanian/acf/ui/theme/Color3.kt

@@ -0,0 +1,62 @@
+// This file is generated using https://material-foundation.github.io/material-theme-builder
+
+package com.sirekanian.acf.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val md_theme_light_primary = Color(0xFFba1e00)
+val md_theme_light_onPrimary = Color(0xFFffffff)
+val md_theme_light_primaryContainer = Color(0xFFffdad1)
+val md_theme_light_onPrimaryContainer = Color(0xFF3f0400)
+val md_theme_light_secondary = Color(0xFF775750)
+val md_theme_light_onSecondary = Color(0xFFffffff)
+val md_theme_light_secondaryContainer = Color(0xFFffdad1)
+val md_theme_light_onSecondaryContainer = Color(0xFF2c1510)
+val md_theme_light_tertiary = Color(0xFF6e5d2e)
+val md_theme_light_onTertiary = Color(0xFFffffff)
+val md_theme_light_tertiaryContainer = Color(0xFFf8e0a6)
+val md_theme_light_onTertiaryContainer = Color(0xFF241a00)
+val md_theme_light_error = Color(0xFFba1b1b)
+val md_theme_light_errorContainer = Color(0xFFffdad4)
+val md_theme_light_onError = Color(0xFFffffff)
+val md_theme_light_onErrorContainer = Color(0xFF410001)
+val md_theme_light_background = Color(0xFFfcfcfc)
+val md_theme_light_onBackground = Color(0xFF201a19)
+val md_theme_light_surface = Color(0xFFfcfcfc)
+val md_theme_light_onSurface = Color(0xFF201a19)
+val md_theme_light_surfaceVariant = Color(0xFFf5ddd8)
+val md_theme_light_onSurfaceVariant = Color(0xFF534340)
+val md_theme_light_outline = Color(0xFF86736f)
+val md_theme_light_inverseOnSurface = Color(0xFFfbeeeb)
+val md_theme_light_inverseSurface = Color(0xFF362f2d)
+val md_theme_light_inversePrimary = Color(0xFFffb4a2)
+val md_theme_light_shadow = Color(0xFF000000)
+val md_theme_dark_primary = Color(0xFFffb4a2)
+val md_theme_dark_onPrimary = Color(0xFF650b00)
+val md_theme_dark_primaryContainer = Color(0xFF8e1400)
+val md_theme_dark_onPrimaryContainer = Color(0xFFffdad1)
+val md_theme_dark_secondary = Color(0xFFe7bdb4)
+val md_theme_dark_onSecondary = Color(0xFF442a24)
+val md_theme_dark_secondaryContainer = Color(0xFF5d3f38)
+val md_theme_dark_onSecondaryContainer = Color(0xFFffdad1)
+val md_theme_dark_tertiary = Color(0xFFdbc48c)
+val md_theme_dark_onTertiary = Color(0xFF3d2f05)
+val md_theme_dark_tertiaryContainer = Color(0xFF544519)
+val md_theme_dark_onTertiaryContainer = Color(0xFFf8e0a6)
+val md_theme_dark_error = Color(0xFFffb4a9)
+val md_theme_dark_errorContainer = Color(0xFF930006)
+val md_theme_dark_onError = Color(0xFF680003)
+val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
+val md_theme_dark_background = Color(0xFF201a19)
+val md_theme_dark_onBackground = Color(0xFFede0dd)
+val md_theme_dark_surface = Color(0xFF201a19)
+val md_theme_dark_onSurface = Color(0xFFede0dd)
+val md_theme_dark_surfaceVariant = Color(0xFF534340)
+val md_theme_dark_onSurfaceVariant = Color(0xFFd8c2bd)
+val md_theme_dark_outline = Color(0xFFa08d89)
+val md_theme_dark_inverseOnSurface = Color(0xFF201a19)
+val md_theme_dark_inverseSurface = Color(0xFFede0dd)
+val md_theme_dark_inversePrimary = Color(0xFFba1e00)
+val md_theme_dark_shadow = Color(0xFF000000)
+val seed = Color(0xFFff3300)
+val error = Color(0xFFba1b1b)

+ 12 - 0
app/src/main/java/com/sirekanian/acf/ui/theme/Shape.kt

@@ -0,0 +1,12 @@
+package com.sirekanian.acf.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+import com.sirekanian.acf.D
+
+val Shapes = Shapes(
+    small = RoundedCornerShape(D.cornerSizeSmall),
+    medium = RoundedCornerShape(D.cornerSizeMedium),
+    large = RoundedCornerShape(0.dp)
+)

+ 75 - 0
app/src/main/java/com/sirekanian/acf/ui/theme/Theme.kt

@@ -0,0 +1,75 @@
+package com.sirekanian.acf.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+private val darkColors = darkColors(
+    primary = md_theme_dark_primary,
+    onPrimary = md_theme_dark_onPrimary,
+//    primaryContainer = md_theme_dark_primaryContainer,
+//    onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+    secondary = md_theme_dark_secondary,
+    onSecondary = md_theme_dark_onSecondary,
+//    secondaryContainer = md_theme_dark_secondaryContainer,
+//    onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+//    tertiary = md_theme_dark_tertiary,
+//    onTertiary = md_theme_dark_onTertiary,
+//    tertiaryContainer = md_theme_dark_tertiaryContainer,
+//    onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+    error = md_theme_dark_error,
+//    errorContainer = md_theme_dark_errorContainer,
+    onError = md_theme_dark_onError,
+//    onErrorContainer = md_theme_dark_onErrorContainer,
+    background = md_theme_dark_background,
+    onBackground = md_theme_dark_onBackground,
+    surface = md_theme_dark_surface,
+    onSurface = md_theme_dark_onSurface,
+//    surfaceVariant = md_theme_dark_surfaceVariant,
+//    onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+//    outline = md_theme_dark_outline,
+//    inverseOnSurface = md_theme_dark_inverseOnSurface,
+//    inverseSurface = md_theme_dark_inverseSurface,
+//    inversePrimary = md_theme_dark_inversePrimary,
+)
+
+private val lightColors = lightColors(
+    primary = md_theme_light_primary,
+    onPrimary = md_theme_light_onPrimary,
+//    primaryContainer = md_theme_light_primaryContainer,
+//    onPrimaryContainer = md_theme_light_onPrimaryContainer,
+    secondary = md_theme_light_secondary,
+    onSecondary = md_theme_light_onSecondary,
+//    secondaryContainer = md_theme_light_secondaryContainer,
+//    onSecondaryContainer = md_theme_light_onSecondaryContainer,
+//    tertiary = md_theme_light_tertiary,
+//    onTertiary = md_theme_light_onTertiary,
+//    tertiaryContainer = md_theme_light_tertiaryContainer,
+//    onTertiaryContainer = md_theme_light_onTertiaryContainer,
+    error = md_theme_light_error,
+//    errorContainer = md_theme_light_errorContainer,
+    onError = md_theme_light_onError,
+//    onErrorContainer = md_theme_light_onErrorContainer,
+    background = md_theme_light_background,
+    onBackground = md_theme_light_onBackground,
+    surface = md_theme_light_surface,
+    onSurface = md_theme_light_onSurface,
+//    surfaceVariant = md_theme_light_surfaceVariant,
+//    onSurfaceVariant = md_theme_light_onSurfaceVariant,
+//    outline = md_theme_light_outline,
+//    inverseOnSurface = md_theme_light_inverseOnSurface,
+//    inverseSurface = md_theme_light_inverseSurface,
+//    inversePrimary = md_theme_light_inversePrimary,
+)
+
+@Composable
+fun WarmongrTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+    MaterialTheme(
+        colors = if (darkTheme) darkColors else lightColors,
+        typography = Typography,
+        shapes = Shapes,
+        content = content
+    )
+}

+ 30 - 0
app/src/main/java/com/sirekanian/acf/ui/theme/Type.kt

@@ -0,0 +1,30 @@
+package com.sirekanian.acf.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import com.sirekanian.acf.R
+
+private val customFontFamily = FontFamily(
+    Font(R.font.montserrat_light, FontWeight.Light),
+    Font(R.font.montserrat_regular, FontWeight.Normal),
+    Font(R.font.montserrat_medium, FontWeight.Medium),
+)
+
+val Typography = Typography(
+    customFontFamily,
+    h1 = Type3.displayLarge,
+    h2 = Type3.displayMedium,
+    h3 = Type3.headlineLarge,
+    h4 = Type3.headlineMedium,
+    h5 = Type3.headlineSmall,
+    h6 = Type3.titleLarge,
+    subtitle1 = Type3.titleMedium,
+    subtitle2 = Type3.titleSmall,
+    body1 = Type3.bodyLarge,
+    body2 = Type3.bodyMedium,
+    button = Type3.labelLarge,
+    caption = Type3.labelMedium,
+    overline = Type3.labelSmall,
+)

+ 100 - 0
app/src/main/java/com/sirekanian/acf/ui/theme/Type3.kt

@@ -0,0 +1,100 @@
+// This file is generated using https://material-foundation.github.io/material-theme-builder
+
+package com.sirekanian.acf.ui.theme
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+object Type3 {
+    val displayLarge = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 57.sp,
+        lineHeight = 64.sp,
+        letterSpacing = (-0.25).sp,
+    )
+    val displayMedium = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 45.sp,
+        lineHeight = 52.sp,
+        letterSpacing = 0.sp,
+    )
+    val displaySmall = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 36.sp,
+        lineHeight = 44.sp,
+        letterSpacing = 0.sp,
+    )
+    val headlineLarge = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 32.sp,
+        lineHeight = 40.sp,
+        letterSpacing = 0.sp,
+    )
+    val headlineMedium = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 28.sp,
+        lineHeight = 36.sp,
+        letterSpacing = 0.sp,
+    )
+    val headlineSmall = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 24.sp,
+        lineHeight = 32.sp,
+        letterSpacing = 0.sp,
+    )
+    val titleLarge = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 22.sp,
+        lineHeight = 28.sp,
+        letterSpacing = 0.sp,
+    )
+    val titleMedium = TextStyle(
+        fontWeight = FontWeight.Medium,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        letterSpacing = 0.1.sp,
+    )
+    val titleSmall = TextStyle(
+        fontWeight = FontWeight.Medium,
+        fontSize = 14.sp,
+        lineHeight = 20.sp,
+        letterSpacing = 0.1.sp,
+    )
+    val bodyLarge = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        letterSpacing = 0.5.sp,
+    )
+    val bodyMedium = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 14.sp,
+        lineHeight = 20.sp,
+        letterSpacing = 0.25.sp,
+    )
+    val bodySmall = TextStyle(
+        fontWeight = FontWeight.Normal,
+        fontSize = 12.sp,
+        lineHeight = 16.sp,
+        letterSpacing = 0.4.sp,
+    )
+    val labelLarge = TextStyle(
+        fontWeight = FontWeight.Medium,
+        fontSize = 14.sp,
+        lineHeight = 20.sp,
+        letterSpacing = 0.1.sp,
+    )
+    val labelMedium = TextStyle(
+        fontWeight = FontWeight.Medium,
+        fontSize = 12.sp,
+        lineHeight = 16.sp,
+        letterSpacing = 0.5.sp,
+    )
+    val labelSmall = TextStyle(
+        fontWeight = FontWeight.Medium,
+        fontSize = 11.sp,
+        lineHeight = 16.sp,
+        letterSpacing = 0.5.sp,
+    )
+}

+ 74 - 0
app/src/main/res/drawable/ic_launcher2_background.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+    android:height="108dp"
+    android:width="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#272a2b"
+          android:pathData="M0,0h108v108h-108z"/>
+    <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+          android:strokeColor="#09000000" android:strokeWidth="0.8"/>
+</vector>

+ 26 - 0
app/src/main/res/drawable/ic_launcher2_foreground.xml

@@ -0,0 +1,26 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.9601293"
+      android:scaleY="0.9601293"
+      android:translateX="37.5"
+      android:translateY="37.78448">
+    <path
+        android:pathData="m30.9687,3.9688l2.8979,0L24.1781,29.8979L21.6262,29.8979L16.9117,17.008 12.2404,29.8979L9.6885,29.8979L0,3.9688L2.8547,3.9688L11.0726,26.4142 15.2249,14.7187 11.4186,4.0185l2.6384,0l2.8547,8.4108 2.8979,-8.4108l2.5951,0l-3.763,10.7001 4.1522,11.6955z"
+        android:strokeWidth="1.1599001"
+        android:fillColor="#c7caca"
+        android:strokeColor="#00000000"/>
+    <path
+        android:pathData="M20.663,27.183C20.1244,25.7103 19.0642,22.8126 18.307,20.7436L16.9302,16.9818 16.0886,15.8398 15.247,14.6977 13.3581,9.3892C12.3192,6.4696 11.4691,4.0746 11.4688,4.067c-0.0002,-0.0075 0.5802,-0.0137 1.2899,-0.0137 0.7096,0 1.2903,0.0071 1.2903,0.0158 0,0.0087 0.6384,1.8926 1.4186,4.1865l1.4186,4.1707 0.8553,1.1256 0.8553,1.1256 2.0734,5.8474c1.1403,3.2161 2.0826,5.859 2.094,5.8732 0.0114,0.0142 0.0401,-0.0056 0.0639,-0.0441 0.0238,-0.0385 1.8693,-5.0842 4.1011,-11.2127l4.0578,-11.1427l1.4176,0c1.3257,0 1.4155,0.0062 1.3854,0.0961 -0.0177,0.0528 -2.1886,5.8652 -4.8242,12.9164l-4.7921,12.8203 -1.2657,0.0147 -1.2657,0.0147z"
+        android:strokeWidth="0.05490501"
+        android:fillColor="#ffb4a9"
+        android:strokeColor="#ffb4a9"/>
+    <path
+        android:pathData="m16.0799,15.8672c-0.4576,-0.6212 -0.8312,-1.1322 -0.8302,-1.1356 0.0018,-0.0061 1.6484,-2.2876 1.6598,-2.2998 0.0033,-0.0035 0.2008,0.2502 0.439,0.5637 0.2382,0.3135 0.6201,0.8163 0.8487,1.1172 0.2286,0.3009 0.4194,0.5555 0.424,0.5657 0.0075,0.0165 -0.0839,0.1422 -0.8358,1.1493 -0.4643,0.6219 -0.8508,1.1394 -0.8589,1.1499 -0.0146,0.019 -0.019,0.0131 -0.8467,-1.1103z"
+        android:strokeWidth="0.008417639"
+        android:fillColor="#e3bfba"
+        android:strokeColor="#ffb4a9"/>
+  </group>
+</vector>

BIN
app/src/main/res/font/montserrat_light.ttf


BIN
app/src/main/res/font/montserrat_medium.ttf


BIN
app/src/main/res/font/montserrat_regular.ttf


+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher2.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher2_background"/>
+    <foreground android:drawable="@drawable/ic_launcher2_foreground"/>
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher2_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher2_background"/>
+    <foreground android:drawable="@drawable/ic_launcher2_foreground"/>
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher2.png


BIN
app/src/main/res/mipmap-hdpi/ic_launcher2_round.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher2.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher2_round.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher2.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher2.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png


+ 82 - 0
app/src/main/res/raw/default1.json

@@ -0,0 +1,82 @@
+[
+  {
+    "0": "Абазалиева Лариса Хасанбиевна",
+    "1": "Abazalieva Larisa Khasanbievna",
+    "2": "23.08.1967",
+    "3": "female",
+    "4": "Chairman of the Election Commission of the Karachay-Cherkess Republic. Head of the state body responsible for supporting the existing political regime in Russia through electoral fraud.",
+    "5": "Election fraud organizers\nHeads of the Central Election Commission and local \nelection commissions"
+  },
+  {
+    "0": "Абакаров Хизри Магомедович",
+    "1": "Abakarov Khizri Magomedovich",
+    "2": "28.06.1960",
+    "3": "male",
+    "4": "Member of the State Duma of the Russian Federation. This state body is responsible for political and legal support of the aggressive war against Ukraine.",
+    "5": "Members of Parliament and Senate\nState Duma"
+  },
+  {
+    "0": "Абашкин Игорь Анатольевич",
+    "1": "Abashkin Igor Anatolievich",
+    "2": "02.03.1970",
+    "3": "male",
+    "4": "Head of the Directorate of the Federal Service of the National Guard Troops of the Russian Federation for the Ulyanovsk Oblast. Heads special military units in the territory under his jurisdiction, which carried out the dispersal of peaceful protests against the Russian invasion of Ukraine and the political regime in Russia.",
+    "5": "Key ‘siloviki’ (security forces) figure\nNational Guard of Russia"
+  },
+  {
+    "0": "Аббазов Ильшат Загфярович",
+    "1": "Abbazov Ilshat Zagfyarovich",
+    "2": "",
+    "3": "male",
+    "4": "Judge of the Moscow City Court. Responsible for the criminal prosecution of citizens of the Russian Federation who oppose the current political regime in Russia.",
+    "5": "Organizers of political repressions\nJudges, prosecutors, state investigators, etc"
+  },
+  {
+    "0": "Абдрахимов Раиф Рамазанович",
+    "1": "Abdrakhimov Raif Ramazanovich",
+    "2": "13.10.1969",
+    "3": "male",
+    "4": "Deputy Prime Minister of the Government of the Republic of Bashkortostan. Manages a regional state body of the Russian Federation that supports or implements actions or policies that undermine or threaten the territorial integrity, sovereignty and independence of Ukraine.",
+    "5": "Key Local Government Officials \nVice-Governors"
+  },
+  {
+    "0": "Абдул-Кадыров Шарпудди Муайдович",
+    "1": "Abdul-Kadyrov Sharpuddi Muaydovich",
+    "2": "05.10.1965",
+    "3": "male",
+    "4": "Prosecutor of the Chechen Republic. Head of the federal state body that provides legal protection for the Russian political regime and refused to protect the rule of law in the context of the invasion of Russian troops in Ukraine.",
+    "5": "Key ‘siloviki’ (security forces) figure \nProsecutor’s Office"
+  },
+  {
+    "0": "Абдулаев М. М.",
+    "1": "Abdulaev M. M.",
+    "2": "",
+    "3": "male",
+    "4": "Investigator for particularly important cases of the first department of investigation of particularly important cases (on crimes against the person and public safety) of the Investigation Department of the RF IC in the Penza Oblast. Involved in the fabrication of the “FBK case”",
+    "5": "Organizers of political repressions\nJudges, prosecutors, state investigators, etc"
+  },
+  {
+    "0": "Абдуллазянов Эдвард Юнусович",
+    "1": "Abdullazyanov Edward Yunusovich",
+    "2": "08.04.1957",
+    "3": "male",
+    "4": "Rector of Kazan State Power Engineering University. Signed an open letter of university rectors expressing support for the decision of the President of Russia to launch a military operation against Ukraine.",
+    "5": "Warmongers\nEducators"
+  },
+  {
+    "0": "Абдулмуслимов Абдулмуслим Мухудинович",
+    "1": "Abdulmuslimov Abdulmuslim Mukhaudinovich",
+    "2": "01.04.1960",
+    "3": "male",
+    "4": "Chairman of the Government Republic of Dagestan. Manages a regional state body of the Russian Federation that supports or implements actions or policies that undermine or threaten the territorial integrity, sovereignty and independence of Ukraine.",
+    "5": "Key Local Government Officials \nVice-Governors"
+  },
+  {
+    "0": "АБДУЛХАЛИМОВ МАГОМЕД СУЛТАНОВИЧ",
+    "1": "Abdulkhalimov Magomed Sultanovich",
+    "2": "02.12.1976",
+    "3": "Male",
+    "4": "Deputy Minister of Labor and Social Protection of the Russian Federation. Manages a state body of the Russian Federation, which supports or implements actions or policies that undermine or threaten the territorial integrity, sovereignty and independence of Ukraine.",
+    "5": "Senior Federal Officials\nExecutive branch and presidential administration employees"
+  }
+]

+ 7 - 0
app/src/main/res/values-be/strings.xml

@@ -0,0 +1,7 @@
+<resources>
+    <string name="app_name_part_1">"Виновники "</string>
+    <string name="app_name_part_2">войны</string>
+    <string name="app_name_part_3" />
+    <string name="app_search_hint">Поиск…</string>
+    <bool name="app_cyrillic">true</bool>
+</resources>

+ 11 - 0
app/src/main/res/values-notnight/themes.xml

@@ -0,0 +1,11 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <style name="Theme.Warmongr" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:windowBackground">@color/app_splash_background_color</item>
+        <item name="android:statusBarColor">#00000000</item>
+        <item name="android:navigationBarColor">#E6FFFFFF</item>
+        <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
+        <item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
+    </style>
+
+</resources>

+ 7 - 0
app/src/main/res/values-ru/strings.xml

@@ -0,0 +1,7 @@
+<resources>
+    <string name="app_name_part_1">"Виновники "</string>
+    <string name="app_name_part_2">войны</string>
+    <string name="app_name_part_3" />
+    <string name="app_search_hint">Поиск…</string>
+    <bool name="app_cyrillic">true</bool>
+</resources>

+ 7 - 0
app/src/main/res/values-uk/strings.xml

@@ -0,0 +1,7 @@
+<resources>
+    <string name="app_name_part_1">"Виновники "</string>
+    <string name="app_name_part_2">войны</string>
+    <string name="app_name_part_3" />
+    <string name="app_search_hint">Поиск…</string>
+    <bool name="app_cyrillic">true</bool>
+</resources>

+ 3 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,3 @@
+<resources>
+    <color name="app_splash_background_color">#191c1d</color>
+</resources>

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,8 @@
+<resources>
+    <string name="app_name" translatable="false">Warmongr</string>
+    <string name="app_name_part_1" />
+    <string name="app_name_part_2">War</string>
+    <string name="app_name_part_3">mongers</string>
+    <string name="app_search_hint">Search…</string>
+    <bool name="app_cyrillic">false</bool>
+</resources>

+ 11 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,11 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <style name="Theme.Warmongr" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:windowBackground">@color/app_splash_background_color</item>
+        <item name="android:statusBarColor">#00000000</item>
+        <item name="android:navigationBarColor">#66000000</item>
+        <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
+        <item name="android:windowLightNavigationBar" tools:ignore="NewApi">false</item>
+    </style>
+
+</resources>

+ 20 - 0
build.gradle

@@ -0,0 +1,20 @@
+buildscript {
+    ext {
+        kotlinVersion = '1.6.21'
+        composeVersion = '1.2.0-beta01'
+        roomVersion = '2.4.2'
+        ktorVersion = '2.0.1'
+    }
+}
+
+plugins {
+    id 'com.android.application' version '7.1.1' apply false
+    id 'com.android.library' version '7.1.1' apply false
+    id 'org.jetbrains.kotlin.android' version "$kotlinVersion" apply false
+    id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion" apply false
+    id 'org.jetbrains.kotlin.kapt' version "$kotlinVersion" apply false
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 4 - 0
gradle.properties

@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Fri Apr 29 20:31:29 GET 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 19 - 0
settings.gradle

@@ -0,0 +1,19 @@
+pluginManagement {
+    repositories {
+        gradlePluginPortal()
+        google()
+        mavenCentral()
+    }
+}
+
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.name = "warmongr"
+
+include ':app'

+ 7 - 0
upd.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+URL="https://sirekanian.github.io/warmongers.json"
+OUTPUT="app/src/main/res/raw/default1.json"
+COUNT="10"
+
+wget -qO- "$URL" | jq ".[0:$COUNT]" >"$OUTPUT"